@pkmn/engine 0.1.0-dev.dfd46bbb → 0.1.0-dev.e0a786f8
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 +51 -23
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +47 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +110 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +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 +15 -18
- package/build/pkg/gen1.js +28 -19
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +42 -91
- package/build/pkg/index.js +23 -70
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +109 -107
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +53 -76
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +152 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +152 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +359 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +33 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +330 -93
- package/package.json +59 -132
- package/src/bin/install-pkmn-engine +31 -20
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +255 -0
- 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 +396 -399
- package/src/lib/common/rational.zig +327 -158
- package/src/lib/common/rng.zig +7 -9
- package/src/lib/common/util.zig +60 -1
- package/src/lib/gen1/calc.zig +696 -160
- 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 +575 -461
- 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 +20 -23
- package/src/lib/gen2/generated.zig +24 -37
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +721 -453
- 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 +93 -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 -330
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,36 +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;
|
|
14
|
+
const Choice = common.Choice;
|
|
24
15
|
const Damage = protocol.Damage;
|
|
25
|
-
const Heal = protocol.Heal;
|
|
26
|
-
|
|
27
|
-
const Gen12 = rng.Gen12;
|
|
28
|
-
|
|
29
|
-
const ActivePokemon = data.ActivePokemon;
|
|
30
16
|
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
18
|
+
const Fail = protocol.Fail;
|
|
19
|
+
const Gen12 = rng.Gen12;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
31
22
|
const Move = data.Move;
|
|
32
23
|
const MoveSlot = data.MoveSlot;
|
|
33
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
34
27
|
const Side = data.Side;
|
|
35
28
|
const Species = data.Species;
|
|
36
29
|
const Stats = data.Stats;
|
|
@@ -70,8 +63,8 @@ pub fn update(battle: anytype, c1: Choice, c2: Choice, options: anytype) !Result
|
|
|
70
63
|
var p1 = battle.side(.P1);
|
|
71
64
|
var p2 = battle.side(.P2);
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
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;
|
|
75
68
|
|
|
76
69
|
if (try turnOrder(battle, c1, c2, options) == .P1) {
|
|
77
70
|
if (try doTurn(battle, .P1, c1, r1, s1, .P2, c2, r2, s2, options)) |r| return r;
|
|
@@ -89,11 +82,11 @@ fn start(battle: anytype, options: anytype) !Result {
|
|
|
89
82
|
const p1 = battle.side(.P1);
|
|
90
83
|
const p2 = battle.side(.P2);
|
|
91
84
|
|
|
92
|
-
|
|
85
|
+
const p1_slot = findFirstAlive(p1);
|
|
93
86
|
assert(!showdown or p1_slot == 1);
|
|
94
87
|
if (p1_slot == 0) return if (findFirstAlive(p2) == 0) Result.Tie else Result.Lose;
|
|
95
88
|
|
|
96
|
-
|
|
89
|
+
const p2_slot = findFirstAlive(p2);
|
|
97
90
|
assert(!showdown or p2_slot == 1);
|
|
98
91
|
if (p2_slot == 0) return Result.Win;
|
|
99
92
|
|
|
@@ -165,7 +158,7 @@ fn selectMove(
|
|
|
165
158
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
166
159
|
// set the selected move will not actually be used, it will just be reported as having
|
|
167
160
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
168
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
161
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
169
162
|
saveMove(battle, player, choice);
|
|
170
163
|
} else {
|
|
171
164
|
assert(choice.data == 0);
|
|
@@ -173,7 +166,10 @@ fn selectMove(
|
|
|
173
166
|
if (foe_choice.type == .Switch) {
|
|
174
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
175
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
176
|
-
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
|
+
}
|
|
177
173
|
}
|
|
178
174
|
}
|
|
179
175
|
return null;
|
|
@@ -252,17 +248,17 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
252
248
|
active.moves = incoming.moves;
|
|
253
249
|
|
|
254
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
255
252
|
|
|
256
253
|
foe.active.volatiles.Binding = false;
|
|
257
254
|
|
|
258
|
-
try options.log.switched(battle.active(player), incoming);
|
|
259
|
-
options.chance.switched(player, side.order[0], out);
|
|
255
|
+
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
256
|
|
|
261
257
|
if (showdown and incoming.status == Status.TOX) {
|
|
262
258
|
incoming.status = Status.init(.PSN);
|
|
263
259
|
// Technically, Pokémon Showdown adds these after *both* Pokémon have switched, but we'd
|
|
264
260
|
// rather not clutter up turnOrder just for this (incorrect) log message
|
|
265
|
-
try options.log.status(battle.active(player), incoming.status, .Silent);
|
|
261
|
+
try options.log.status(.{ battle.active(player), incoming.status, .Silent });
|
|
266
262
|
}
|
|
267
263
|
}
|
|
268
264
|
|
|
@@ -346,13 +342,13 @@ fn doTurn(
|
|
|
346
342
|
)) |r| return r;
|
|
347
343
|
if (!replace) {
|
|
348
344
|
if (player_choice.type != .Switch) {
|
|
349
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
350
346
|
}
|
|
351
347
|
if (residual) try handleResidual(battle, player, options);
|
|
352
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
348
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
353
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
354
350
|
|
|
355
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
356
352
|
|
|
357
353
|
residual = true;
|
|
358
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -368,10 +364,10 @@ fn doTurn(
|
|
|
368
364
|
)) |r| return r;
|
|
369
365
|
if (!replace) {
|
|
370
366
|
if (!calc and foe_choice.type != .Switch) {
|
|
371
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
367
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
372
368
|
}
|
|
373
369
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
374
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
370
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
375
371
|
}
|
|
376
372
|
|
|
377
373
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -406,7 +402,7 @@ fn executeMove(
|
|
|
406
402
|
if (side.last_selected_move == .SKIP_TURN) {
|
|
407
403
|
assert(!showdown);
|
|
408
404
|
if (battle.foe(player).active.volatiles.Binding) {
|
|
409
|
-
try options.log.cant(battle.active(player), .Bound);
|
|
405
|
+
try options.log.cant(.{ battle.active(player), .Bound });
|
|
410
406
|
}
|
|
411
407
|
return null;
|
|
412
408
|
}
|
|
@@ -424,13 +420,13 @@ fn executeMove(
|
|
|
424
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
425
421
|
const stored = side.stored();
|
|
426
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
427
|
-
auto = isForced(&side.active) or
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
428
424
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
429
425
|
side.last_selected_move == .HyperBeam or
|
|
430
426
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
431
427
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
432
428
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
433
|
-
// 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
|
|
434
430
|
if (!auto) {
|
|
435
431
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
436
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -457,6 +453,25 @@ fn executeMove(
|
|
|
457
453
|
.err => return @as(?Result, Result.Error),
|
|
458
454
|
}
|
|
459
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
|
+
|
|
460
475
|
const can = skip_can or
|
|
461
476
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
462
477
|
if (!can) return null;
|
|
@@ -483,36 +498,41 @@ fn beforeMove(
|
|
|
483
498
|
|
|
484
499
|
if (Status.is(stored.status, .SLP)) {
|
|
485
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
486
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
487
|
-
if (options.calc.
|
|
488
|
-
|
|
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,
|
|
489
509
|
} else {
|
|
490
510
|
stored.status -= 1;
|
|
491
511
|
}
|
|
492
512
|
|
|
493
513
|
const duration = Status.duration(stored.status);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
497
517
|
|
|
498
518
|
if (duration == 0) {
|
|
499
|
-
try log.curestatus(ident, before, .Message);
|
|
519
|
+
try log.curestatus(.{ ident, before, .Message });
|
|
500
520
|
stored.status = 0; // clears EXT if present
|
|
501
521
|
} else {
|
|
502
|
-
try log.cant(ident, .Sleep);
|
|
522
|
+
try log.cant(.{ ident, .Sleep });
|
|
503
523
|
}
|
|
504
524
|
side.last_used_move = .None;
|
|
505
525
|
return .done;
|
|
506
526
|
}
|
|
507
527
|
|
|
508
528
|
if (Status.is(stored.status, .FRZ)) {
|
|
509
|
-
try log.cant(ident, .Freeze);
|
|
529
|
+
try log.cant(.{ ident, .Freeze });
|
|
510
530
|
side.last_used_move = .None;
|
|
511
531
|
return .done;
|
|
512
532
|
}
|
|
513
533
|
|
|
514
534
|
if (skip or foe.active.volatiles.Binding) {
|
|
515
|
-
try log.cant(ident, .Bound);
|
|
535
|
+
try log.cant(.{ ident, .Bound });
|
|
516
536
|
return .done;
|
|
517
537
|
}
|
|
518
538
|
|
|
@@ -520,27 +540,27 @@ fn beforeMove(
|
|
|
520
540
|
// Pokémon Showdown doesn't clear Flinch until its imaginary "residual" phase, meaning
|
|
521
541
|
// Pokémon can sometimes flinch multiple times from the same original hit
|
|
522
542
|
if (!showdown) volatiles.Flinch = false;
|
|
523
|
-
try log.cant(ident, .Flinch);
|
|
543
|
+
try log.cant(.{ ident, .Flinch });
|
|
524
544
|
return .done;
|
|
525
545
|
}
|
|
526
546
|
|
|
527
547
|
if (volatiles.Recharging) {
|
|
528
548
|
volatiles.Recharging = false;
|
|
529
|
-
try log.cant(ident, .Recharge);
|
|
549
|
+
try log.cant(.{ ident, .Recharge });
|
|
530
550
|
return .done;
|
|
531
551
|
}
|
|
532
552
|
|
|
533
553
|
if (volatiles.disable_duration > 0) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
+
);
|
|
540
560
|
|
|
541
561
|
if (volatiles.disable_duration == 0) {
|
|
542
562
|
volatiles.disable_move = 0;
|
|
543
|
-
try log.end(ident, .Disable);
|
|
563
|
+
try log.end(.{ ident, .Disable });
|
|
544
564
|
}
|
|
545
565
|
}
|
|
546
566
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
@@ -549,25 +569,30 @@ fn beforeMove(
|
|
|
549
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
550
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
551
571
|
if (side.last_selected_move == .None) {
|
|
552
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
553
573
|
return .err;
|
|
554
574
|
}
|
|
555
575
|
|
|
556
576
|
if (volatiles.Confusion) {
|
|
557
577
|
assert(volatiles.confusion > 0);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
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,
|
|
561
585
|
} else {
|
|
562
586
|
volatiles.confusion -= 1;
|
|
563
587
|
}
|
|
564
|
-
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
565
590
|
|
|
566
591
|
if (volatiles.confusion == 0) {
|
|
567
592
|
volatiles.Confusion = false;
|
|
568
|
-
try log.end(ident, .Confusion);
|
|
593
|
+
try log.end(.{ ident, .Confusion });
|
|
569
594
|
} else {
|
|
570
|
-
try log.activate(ident, .Confusion);
|
|
595
|
+
try log.activate(.{ ident, .Confusion });
|
|
571
596
|
|
|
572
597
|
if (try Rolls.confused(battle, player, options)) {
|
|
573
598
|
assert(!volatiles.MultiHit);
|
|
@@ -579,7 +604,9 @@ fn beforeMove(
|
|
|
579
604
|
volatiles.Charging = false;
|
|
580
605
|
volatiles.Binding = false;
|
|
581
606
|
volatiles.Invulnerable = false;
|
|
582
|
-
|
|
607
|
+
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
583
610
|
{
|
|
584
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
585
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -587,19 +614,14 @@ fn beforeMove(
|
|
|
587
614
|
const def = foe.active.stats.def;
|
|
588
615
|
foe.active.stats.def = active.stats.def;
|
|
589
616
|
defer foe.active.stats.def = def;
|
|
590
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
591
620
|
}
|
|
592
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
593
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
594
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
595
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
596
|
-
player
|
|
597
|
-
else
|
|
598
|
-
player.foe();
|
|
599
621
|
|
|
600
622
|
const uncapped = battle.last_damage;
|
|
601
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
602
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
603
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
604
626
|
if (showdown) battle.last_damage = uncapped;
|
|
605
627
|
|
|
@@ -616,9 +638,12 @@ fn beforeMove(
|
|
|
616
638
|
volatiles.Thrashing = false;
|
|
617
639
|
volatiles.Charging = false;
|
|
618
640
|
volatiles.Binding = false;
|
|
619
|
-
|
|
641
|
+
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
643
|
+
options.chance.observe(.binding, player, .None);
|
|
644
|
+
|
|
620
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
621
|
-
try log.cant(ident, .Paralysis);
|
|
646
|
+
try log.cant(.{ ident, .Paralysis });
|
|
622
647
|
return .done;
|
|
623
648
|
}
|
|
624
649
|
|
|
@@ -634,28 +659,22 @@ fn beforeMove(
|
|
|
634
659
|
volatiles.state +%= battle.last_damage;
|
|
635
660
|
}
|
|
636
661
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (options.calc.modify(player, .attacking)) |extend| {
|
|
640
|
-
if (!extend) volatiles.attacks = 0;
|
|
641
|
-
} else {
|
|
642
|
-
volatiles.attacks -= 1;
|
|
643
|
-
}
|
|
644
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
645
664
|
|
|
646
665
|
if (volatiles.attacks != 0) {
|
|
647
|
-
try log.activate(ident, .Bide);
|
|
666
|
+
try log.activate(.{ ident, .Bide });
|
|
648
667
|
return .done;
|
|
649
668
|
}
|
|
650
669
|
|
|
651
670
|
volatiles.Bide = false;
|
|
652
|
-
try log.end(ident, .Bide);
|
|
671
|
+
try log.end(.{ ident, .Bide });
|
|
653
672
|
|
|
654
673
|
battle.last_damage = volatiles.state *% 2;
|
|
655
674
|
volatiles.state = 0;
|
|
656
675
|
|
|
657
676
|
if (battle.last_damage == 0) {
|
|
658
|
-
try log.fail(ident, .None);
|
|
677
|
+
try log.fail(.{ ident, .None });
|
|
659
678
|
return .done;
|
|
660
679
|
}
|
|
661
680
|
|
|
@@ -675,41 +694,42 @@ fn beforeMove(
|
|
|
675
694
|
}
|
|
676
695
|
|
|
677
696
|
if (volatiles.Thrashing) {
|
|
678
|
-
|
|
697
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
679
698
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
} else {
|
|
683
|
-
volatiles.attacks -= 1;
|
|
684
|
-
}
|
|
685
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
686
701
|
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
702
|
+
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
705
|
+
volatiles.Thrashing = false;
|
|
706
|
+
volatiles.Confusion = true;
|
|
707
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
708
|
+
|
|
709
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
710
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
711
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
712
|
+
// correct player)
|
|
713
|
+
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
714
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
715
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
716
|
+
else
|
|
717
|
+
.started);
|
|
718
|
+
if (!overwritten) options.calc.confusion(player);
|
|
698
719
|
}
|
|
720
|
+
|
|
721
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
722
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
723
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
724
|
+
|
|
699
725
|
return .skip_can;
|
|
700
726
|
}
|
|
701
727
|
|
|
702
728
|
if (volatiles.Binding) {
|
|
703
|
-
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
704
731
|
|
|
705
|
-
|
|
706
|
-
if (!extend) volatiles.attacks = 0;
|
|
707
|
-
} else {
|
|
708
|
-
volatiles.attacks -= 1;
|
|
709
|
-
}
|
|
710
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
711
|
-
|
|
712
|
-
try log.move(battle.active(player), side.last_selected_move, battle.active(player.foe()));
|
|
732
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
713
733
|
if (showdown or battle.last_damage != 0) {
|
|
714
734
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
715
735
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -737,22 +757,21 @@ fn canMove(
|
|
|
737
757
|
const player_ident = battle.active(player);
|
|
738
758
|
const move = Move.get(side.last_selected_move);
|
|
739
759
|
|
|
740
|
-
var skip = skip_pp;
|
|
741
760
|
if (side.active.volatiles.Charging) {
|
|
742
761
|
side.active.volatiles.Charging = false;
|
|
743
762
|
side.active.volatiles.Invulnerable = false;
|
|
744
763
|
} else if (move.effect == .Charge) {
|
|
745
|
-
try options.log.
|
|
764
|
+
try options.log.move(.{ player_ident, side.last_selected_move, ID{}, from });
|
|
746
765
|
setCounterable(battle, player, side, move);
|
|
747
766
|
try Effects.charge(battle, player, options);
|
|
748
767
|
return false;
|
|
749
768
|
}
|
|
750
769
|
|
|
751
770
|
side.last_used_move = side.last_selected_move;
|
|
752
|
-
if (!
|
|
771
|
+
if (!skip_pp) decrementPP(side, mslot, auto);
|
|
753
772
|
|
|
754
773
|
const target = if (move.target == .Self) player else player.foe();
|
|
755
|
-
try options.log.
|
|
774
|
+
try options.log.move(.{ player_ident, side.last_selected_move, battle.active(target), from });
|
|
756
775
|
setCounterable(battle, player, side, move);
|
|
757
776
|
|
|
758
777
|
if (move.effect.onBegin()) {
|
|
@@ -762,9 +781,14 @@ fn canMove(
|
|
|
762
781
|
|
|
763
782
|
if (move.effect == .Thrashing) {
|
|
764
783
|
Effects.thrashing(battle, player, options);
|
|
765
|
-
} else if (
|
|
766
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
767
|
-
|
|
784
|
+
} else if (move.effect == .Binding) {
|
|
785
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
786
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
787
|
+
if (showdown) {
|
|
788
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
789
|
+
} else {
|
|
790
|
+
try Effects.binding(battle, player, false, options);
|
|
791
|
+
}
|
|
768
792
|
}
|
|
769
793
|
|
|
770
794
|
return true;
|
|
@@ -801,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
801
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
802
826
|
}
|
|
803
827
|
|
|
804
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
805
829
|
var active = &side.active;
|
|
806
830
|
const volatiles = &active.volatiles;
|
|
807
831
|
|
|
@@ -809,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
809
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
810
834
|
if (showdown and volatiles.Transform) return;
|
|
811
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
812
839
|
assert(mslot > 0 and mslot <= 4);
|
|
813
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
840
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
814
841
|
}
|
|
815
842
|
|
|
816
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -858,13 +885,13 @@ fn doMove(
|
|
|
858
885
|
break :miss true;
|
|
859
886
|
}
|
|
860
887
|
break :miss (move.target != .Self and
|
|
861
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
888
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
862
889
|
};
|
|
863
890
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
864
891
|
|
|
865
892
|
var late = showdown and move.effect != .Explode;
|
|
866
893
|
const skip = status or immune;
|
|
867
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
868
895
|
if (showdown and move.effect.isMulti()) {
|
|
869
896
|
try Effects.multiHit(battle, player, move, options);
|
|
870
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -886,8 +913,9 @@ fn doMove(
|
|
|
886
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
887
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
888
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
889
|
-
if (showdown) break :
|
|
890
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
916
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
917
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
918
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
891
919
|
return @as(?Result, Result.Error);
|
|
892
920
|
}
|
|
893
921
|
if (battle.last_damage == 0) {
|
|
@@ -901,11 +929,12 @@ fn doMove(
|
|
|
901
929
|
}
|
|
902
930
|
}
|
|
903
931
|
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
932
|
+
var missed: bool = undefined;
|
|
933
|
+
const zero = battle.last_damage == 0;
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
909
938
|
|
|
910
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
911
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -929,16 +958,37 @@ fn doMove(
|
|
|
929
958
|
ohko = (!showdown or (!immune and !invulnerable)) and
|
|
930
959
|
move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe;
|
|
931
960
|
if (ohko) {
|
|
932
|
-
try log.immune(foe_ident, .OHKO);
|
|
961
|
+
try log.immune(.{ foe_ident, .OHKO });
|
|
933
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
934
|
-
try log.immune(foe_ident, .None);
|
|
963
|
+
try log.immune(.{ foe_ident, .None });
|
|
964
|
+
if (!showdown) {
|
|
965
|
+
if (move.effect == .Binding) {
|
|
966
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
967
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
968
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
969
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
970
|
+
// (considered missing)
|
|
971
|
+
assert(zero);
|
|
972
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
973
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
974
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
975
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
976
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
977
|
+
// roll)
|
|
978
|
+
try options.chance.commit(player, .binding);
|
|
979
|
+
} else {
|
|
980
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
981
|
+
// division-by-zero freeze alternative timeline scenario
|
|
982
|
+
try options.chance.commit(player, .glitch);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
935
985
|
} else if (mist) {
|
|
936
|
-
if (!foe.active.volatiles.Substitute) try log.activate(foe_ident, .Mist);
|
|
937
|
-
try log.fail(foe_ident, .None);
|
|
986
|
+
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
987
|
+
try log.fail(.{ foe_ident, .None });
|
|
938
988
|
} else {
|
|
939
|
-
if (showdown
|
|
940
|
-
try log.lastmiss();
|
|
941
|
-
try log.miss(battle.active(player));
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
990
|
+
try log.lastmiss(.{});
|
|
991
|
+
try log.miss(.{battle.active(player)});
|
|
942
992
|
}
|
|
943
993
|
if (move.effect == .JumpKick) {
|
|
944
994
|
// Recoil is supposed to be damage/8 but damage will always be 0 here
|
|
@@ -951,17 +1001,15 @@ fn doMove(
|
|
|
951
1001
|
try buildRage(battle, player.foe(), options);
|
|
952
1002
|
} else if (showdown and move.effect == .Disable) {
|
|
953
1003
|
try buildRage(battle, player.foe(), options);
|
|
954
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
955
|
-
try options.chance.commit(player, .binding);
|
|
956
1004
|
}
|
|
957
1005
|
return null;
|
|
958
|
-
} else {
|
|
1006
|
+
} else if (!showdown) {
|
|
959
1007
|
try options.chance.commit(player, .hit);
|
|
960
1008
|
}
|
|
961
1009
|
|
|
962
1010
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
963
1011
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
964
|
-
// it up above before damage calculation)
|
|
1012
|
+
// it up above before damage calculation)
|
|
965
1013
|
if (!showdown and move.effect.isMulti()) {
|
|
966
1014
|
try Effects.multiHit(battle, player, move, options);
|
|
967
1015
|
hits = side.active.volatiles.attacks;
|
|
@@ -974,15 +1022,15 @@ fn doMove(
|
|
|
974
1022
|
var hit: u4 = 0;
|
|
975
1023
|
while (hit < hits) {
|
|
976
1024
|
if (hit == 0) {
|
|
977
|
-
if (crit) try log.crit(battle.active(player.foe()));
|
|
1025
|
+
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
978
1026
|
if (effectiveness > Effectiveness.neutral) {
|
|
979
|
-
try log.supereffective(battle.active(player.foe()));
|
|
1027
|
+
try log.supereffective(.{battle.active(player.foe())});
|
|
980
1028
|
} else if (effectiveness < Effectiveness.neutral) {
|
|
981
|
-
try log.resisted(battle.active(player.foe()));
|
|
1029
|
+
try log.resisted(.{battle.active(player.foe())});
|
|
982
1030
|
}
|
|
983
1031
|
}
|
|
984
1032
|
if (!skip) nullified = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
985
|
-
if (hit == 0 and ohko) try log.ohko();
|
|
1033
|
+
if (hit == 0 and ohko) try log.ohko(.{});
|
|
986
1034
|
hit += 1;
|
|
987
1035
|
if (foe.stored().hp == 0) break;
|
|
988
1036
|
if (!late and (!sub or move.effect == .Explode)) {
|
|
@@ -999,33 +1047,31 @@ fn doMove(
|
|
|
999
1047
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
1000
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1001
1049
|
}
|
|
1002
|
-
try log.hitcount(battle.active(player.foe()), hit);
|
|
1003
|
-
} else if (showdown) {
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
1004
1052
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
try buildRage(battle, player.foe(), options);
|
|
1016
|
-
}
|
|
1017
|
-
return null;
|
|
1053
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1054
|
+
if (immune) {
|
|
1055
|
+
battle.last_damage = 0;
|
|
1056
|
+
assert(foe.stored().hp > 0);
|
|
1057
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1058
|
+
if (sub) {
|
|
1059
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1060
|
+
} else {
|
|
1061
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1062
|
+
try buildRage(battle, player.foe(), options);
|
|
1018
1063
|
}
|
|
1064
|
+
return null;
|
|
1019
1065
|
}
|
|
1020
1066
|
}
|
|
1021
1067
|
|
|
1022
1068
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1023
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1069
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1024
1070
|
if (nullified) return null;
|
|
1025
1071
|
|
|
1026
1072
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1027
1073
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1028
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1074
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1029
1075
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1030
1076
|
|
|
1031
1077
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1040,7 +1086,7 @@ fn doMove(
|
|
|
1040
1086
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1041
1087
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1042
1088
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1043
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1089
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1044
1090
|
if (move.effect == .Twineedle) {
|
|
1045
1091
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1046
1092
|
} else if (move.effect == .Disable) {
|
|
@@ -1059,12 +1105,14 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1059
1105
|
const side = battle.side(player);
|
|
1060
1106
|
|
|
1061
1107
|
// Base speed is used for the critical hit calculation, even when Transform-ed
|
|
1062
|
-
var rate =
|
|
1108
|
+
var rate: u16 = Species.chance(side.stored().species);
|
|
1063
1109
|
// GLITCH: Focus Energy reduces critical hit rate instead of increasing it
|
|
1064
1110
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1065
1111
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1066
1112
|
|
|
1067
|
-
|
|
1113
|
+
const spurious =
|
|
1114
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1115
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1068
1116
|
}
|
|
1069
1117
|
|
|
1070
1118
|
fn calcDamage(
|
|
@@ -1073,6 +1121,7 @@ fn calcDamage(
|
|
|
1073
1121
|
target_player: Player,
|
|
1074
1122
|
m: ?Move.Data,
|
|
1075
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1076
1125
|
) bool {
|
|
1077
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1078
1127
|
const cfz = m == null;
|
|
@@ -1084,28 +1133,8 @@ fn calcDamage(
|
|
|
1084
1133
|
|
|
1085
1134
|
const special = move.type.special();
|
|
1086
1135
|
|
|
1087
|
-
|
|
1088
|
-
var
|
|
1089
|
-
if (crit)
|
|
1090
|
-
if (special) side.stored().stats.spc
|
|
1091
|
-
else side.stored().stats.atk
|
|
1092
|
-
else
|
|
1093
|
-
if (special) side.active.stats.spc
|
|
1094
|
-
else side.active.stats.atk;
|
|
1095
|
-
var def: u32 =
|
|
1096
|
-
if (crit)
|
|
1097
|
-
if (special) target.stored().stats.spc
|
|
1098
|
-
else target.stored().stats.def
|
|
1099
|
-
else
|
|
1100
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1101
|
-
if (special)
|
|
1102
|
-
target.active.stats.spc *
|
|
1103
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1104
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1105
|
-
else
|
|
1106
|
-
target.active.stats.def *
|
|
1107
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1108
|
-
// zig fmt: on
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1109
1138
|
|
|
1110
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1111
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1113,12 +1142,12 @@ fn calcDamage(
|
|
|
1113
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1114
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1115
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1116
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1117
1146
|
}
|
|
1118
1147
|
|
|
1119
|
-
const lvl =
|
|
1148
|
+
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
1120
1149
|
|
|
1121
|
-
def =
|
|
1150
|
+
def = if (move.effect == .Explode) @max(def / 2, 1) else def;
|
|
1122
1151
|
|
|
1123
1152
|
if (def == 0) return false;
|
|
1124
1153
|
|
|
@@ -1130,11 +1159,41 @@ fn calcDamage(
|
|
|
1130
1159
|
d = @min(997, d);
|
|
1131
1160
|
d += 2;
|
|
1132
1161
|
|
|
1162
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1163
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1164
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1165
|
+
// that results in counter intuitive probabilities in the common case
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1167
|
+
atk = attack(side, !crit, special);
|
|
1168
|
+
def = defense(target, !crit, special, false);
|
|
1169
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1133
1172
|
battle.last_damage = @intCast(d);
|
|
1134
1173
|
|
|
1135
1174
|
return true;
|
|
1136
1175
|
}
|
|
1137
1176
|
|
|
1177
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1180
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1186
|
+
else
|
|
1187
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1188
|
+
if (special)
|
|
1189
|
+
target.active.stats.spc *
|
|
1190
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1191
|
+
else
|
|
1192
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1193
|
+
target.active.stats.def *
|
|
1194
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1138
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1139
1198
|
const side = battle.side(player);
|
|
1140
1199
|
const foe = battle.foe(player);
|
|
@@ -1179,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1179
1238
|
const foe = battle.foe(player);
|
|
1180
1239
|
|
|
1181
1240
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1182
|
-
try options.chance.commit(player, .hit);
|
|
1183
1241
|
|
|
1184
1242
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1185
1243
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1190,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1190
1248
|
.Psywave => power: {
|
|
1191
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1192
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1193
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1194
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1195
1253
|
},
|
|
1196
1254
|
else => unreachable,
|
|
1197
1255
|
};
|
|
1198
1256
|
|
|
1199
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1200
1258
|
|
|
1201
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1202
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1209,7 +1267,7 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1209
1267
|
const foe = battle.foe(player);
|
|
1210
1268
|
|
|
1211
1269
|
if (battle.last_damage == 0) {
|
|
1212
|
-
try options.log.fail(battle.active(player), .None);
|
|
1270
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1213
1271
|
return null;
|
|
1214
1272
|
}
|
|
1215
1273
|
|
|
@@ -1227,22 +1285,25 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1227
1285
|
foe_last_selected_move.type == .Fighting);
|
|
1228
1286
|
|
|
1229
1287
|
if (!used and !selected) {
|
|
1230
|
-
try options.log.fail(battle.active(player), .None);
|
|
1288
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1231
1289
|
return null;
|
|
1232
1290
|
}
|
|
1233
1291
|
|
|
1234
1292
|
if (!used or !selected) {
|
|
1235
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1236
|
-
if (!
|
|
1237
|
-
try options.log.fail(battle.active(player), .None);
|
|
1294
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1295
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1238
1296
|
return null;
|
|
1239
1297
|
}
|
|
1240
1298
|
|
|
1241
|
-
|
|
1299
|
+
// NOTE: llvm/llvm-project#58557
|
|
1300
|
+
battle.last_damage = if (comptime builtin.target.isWasm())
|
|
1301
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1302
|
+
else
|
|
1303
|
+
battle.last_damage *| 2;
|
|
1242
1304
|
|
|
1243
|
-
// Pokémon Showdown calls
|
|
1305
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1244
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1245
|
-
try options.chance.commit(player, .hit);
|
|
1246
1307
|
|
|
1247
1308
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1248
1309
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1270,20 +1331,20 @@ fn applyDamage(
|
|
|
1270
1331
|
subbed.active.volatiles.substitute = 0;
|
|
1271
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1272
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1273
|
-
try options.log.end(battle.active(sub_player), .Substitute);
|
|
1274
|
-
options.calc.capped(
|
|
1334
|
+
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1275
1336
|
return true;
|
|
1276
1337
|
} else {
|
|
1277
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
1278
1339
|
subbed.active.volatiles.substitute -= @intCast(battle.last_damage);
|
|
1279
|
-
try options.log.activate(battle.active(sub_player), .Substitute);
|
|
1340
|
+
try options.log.activate(.{ battle.active(sub_player), .Substitute });
|
|
1280
1341
|
return false;
|
|
1281
1342
|
}
|
|
1282
1343
|
}
|
|
1283
1344
|
|
|
1284
1345
|
if (battle.last_damage > target.stored().hp) battle.last_damage = target.stored().hp;
|
|
1285
1346
|
target.stored().hp -= battle.last_damage;
|
|
1286
|
-
try options.log.damage(battle.active(target_player), target.stored(), reason);
|
|
1347
|
+
try options.log.damage(.{ battle.active(target_player), target.stored(), reason });
|
|
1287
1348
|
return false;
|
|
1288
1349
|
}
|
|
1289
1350
|
|
|
@@ -1302,10 +1363,10 @@ fn mirrorMove(
|
|
|
1302
1363
|
side.last_selected_move = foe.last_used_move;
|
|
1303
1364
|
|
|
1304
1365
|
if (foe.last_used_move == .None or foe.last_used_move == .MirrorMove) {
|
|
1305
|
-
try options.log.fail(battle.active(player), .None);
|
|
1366
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
1306
1367
|
return null;
|
|
1307
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1308
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1309
1370
|
}
|
|
1310
1371
|
|
|
1311
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1326,7 +1387,7 @@ fn metronome(
|
|
|
1326
1387
|
var side = battle.side(player);
|
|
1327
1388
|
|
|
1328
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1329
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1330
1391
|
|
|
1331
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1332
1393
|
return null;
|
|
@@ -1338,20 +1399,23 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1338
1399
|
var immune = false;
|
|
1339
1400
|
var mist = false;
|
|
1340
1401
|
|
|
1341
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1402
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1342
1406
|
|
|
1343
1407
|
assert(!immune);
|
|
1344
1408
|
if (mist) {
|
|
1345
1409
|
assert(!showdown);
|
|
1346
1410
|
const foe_ident = battle.active(player.foe());
|
|
1347
|
-
try options.log.activate(foe_ident, .Mist);
|
|
1348
|
-
try options.log.fail(foe_ident, .None);
|
|
1411
|
+
try options.log.activate(.{ foe_ident, .Mist });
|
|
1412
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1349
1413
|
} else {
|
|
1350
|
-
try options.
|
|
1351
|
-
try options.log.
|
|
1352
|
-
try options.log.miss(battle.active(player));
|
|
1414
|
+
try options.log.lastmiss(.{});
|
|
1415
|
+
try options.log.miss(.{battle.active(player)});
|
|
1353
1416
|
}
|
|
1354
1417
|
|
|
1418
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1355
1419
|
return false;
|
|
1356
1420
|
}
|
|
1357
1421
|
|
|
@@ -1362,7 +1426,7 @@ fn moveHit(
|
|
|
1362
1426
|
immune: *bool,
|
|
1363
1427
|
mist: *bool,
|
|
1364
1428
|
options: anytype,
|
|
1365
|
-
) bool {
|
|
1429
|
+
) !bool {
|
|
1366
1430
|
var side = battle.side(player);
|
|
1367
1431
|
const foe = battle.foe(player);
|
|
1368
1432
|
|
|
@@ -1401,7 +1465,7 @@ fn moveHit(
|
|
|
1401
1465
|
const overwritten = overwrite and state > 0;
|
|
1402
1466
|
assert(!overwritten or (0 < state and state <= 255 and !side.active.volatiles.Bide));
|
|
1403
1467
|
|
|
1404
|
-
var accuracy = if (overwritten) state else
|
|
1468
|
+
var accuracy: u16 = if (overwritten) state else move.accuracy;
|
|
1405
1469
|
var boost = BOOSTS[@as(u4, @intCast(@as(i8, side.active.boosts.accuracy) + 6))];
|
|
1406
1470
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1407
1471
|
boost = BOOSTS[@as(u4, @intCast(@as(i8, -foe.active.boosts.evasion) + 6))];
|
|
@@ -1413,7 +1477,7 @@ fn moveHit(
|
|
|
1413
1477
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1414
1478
|
|
|
1415
1479
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1416
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1480
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1417
1481
|
};
|
|
1418
1482
|
|
|
1419
1483
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1424,13 +1488,17 @@ fn moveHit(
|
|
|
1424
1488
|
|
|
1425
1489
|
if (!miss) return true;
|
|
1426
1490
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1491
|
+
|
|
1427
1492
|
side.active.volatiles.Binding = false;
|
|
1493
|
+
options.chance.observe(.binding, player, .None);
|
|
1494
|
+
|
|
1428
1495
|
return false;
|
|
1429
1496
|
}
|
|
1430
1497
|
|
|
1431
1498
|
fn checkFaint(
|
|
1432
1499
|
battle: anytype,
|
|
1433
1500
|
player: Player,
|
|
1501
|
+
cap: bool,
|
|
1434
1502
|
options: anytype,
|
|
1435
1503
|
) @TypeOf(options.log).Error!?Result {
|
|
1436
1504
|
const side = battle.side(player);
|
|
@@ -1443,20 +1511,20 @@ fn checkFaint(
|
|
|
1443
1511
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1444
1512
|
const more = player_out or foe_out;
|
|
1445
1513
|
|
|
1446
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1447
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1514
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1515
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1448
1516
|
|
|
1449
1517
|
assert(!side.active.volatiles.MultiHit);
|
|
1450
1518
|
assert(!foe.active.volatiles.MultiHit);
|
|
1451
1519
|
|
|
1452
1520
|
if (player_out and foe_out) {
|
|
1453
|
-
try options.log.tie();
|
|
1521
|
+
try options.log.tie(.{});
|
|
1454
1522
|
return Result.Tie;
|
|
1455
1523
|
} else if (player_out) {
|
|
1456
|
-
try options.log.win(player.foe());
|
|
1524
|
+
try options.log.win(.{player.foe()});
|
|
1457
1525
|
return if (player == .P1) Result.Lose else Result.Win;
|
|
1458
1526
|
} else if (foe_out) {
|
|
1459
|
-
try options.log.win(player);
|
|
1527
|
+
try options.log.win(.{player});
|
|
1460
1528
|
return if (player == .P1) Result.Win else Result.Lose;
|
|
1461
1529
|
}
|
|
1462
1530
|
|
|
@@ -1465,7 +1533,7 @@ fn checkFaint(
|
|
|
1465
1533
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1466
1534
|
}
|
|
1467
1535
|
|
|
1468
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1536
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1469
1537
|
var side = battle.side(player);
|
|
1470
1538
|
var foe = battle.foe(player);
|
|
1471
1539
|
assert(side.stored().hp == 0);
|
|
@@ -1474,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1474
1542
|
assert(!foe_volatiles.MultiHit);
|
|
1475
1543
|
if (foe_volatiles.Bide) {
|
|
1476
1544
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1477
|
-
foe_volatiles.state = if (
|
|
1545
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1478
1546
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1479
1547
|
}
|
|
1480
1548
|
|
|
@@ -1498,8 +1566,10 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1498
1566
|
side.stored().stats.spe;
|
|
1499
1567
|
}
|
|
1500
1568
|
|
|
1501
|
-
try options.log.faint(battle.active(player), done);
|
|
1502
|
-
|
|
1569
|
+
try options.log.faint(.{ battle.active(player), done });
|
|
1570
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1571
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1572
|
+
if (cap) options.calc.capped(player);
|
|
1503
1573
|
return null;
|
|
1504
1574
|
}
|
|
1505
1575
|
|
|
@@ -1520,8 +1590,8 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1520
1590
|
|
|
1521
1591
|
if (!showdown or damage > 0) {
|
|
1522
1592
|
stored.hp -= damage;
|
|
1523
|
-
// Pokémon Showdown uses
|
|
1524
|
-
try options.log.damage(ident, stored, if (brn) Damage.Burn else Damage.Poison);
|
|
1593
|
+
// Pokémon Showdown uses damage [of] here but its not relevant in Generation I
|
|
1594
|
+
try options.log.damage(.{ ident, stored, if (brn) Damage.Burn else Damage.Poison });
|
|
1525
1595
|
}
|
|
1526
1596
|
}
|
|
1527
1597
|
|
|
@@ -1546,14 +1616,14 @@ fn handleResidual(battle: anytype, player: Player, options: anytype) !void {
|
|
|
1546
1616
|
const amount = @min(damage, stored.hp);
|
|
1547
1617
|
stored.hp -= amount;
|
|
1548
1618
|
|
|
1549
|
-
// As above, Pokémon Showdown uses
|
|
1550
|
-
if (amount > 0) try options.log.damage(ident, stored, .LeechSeed);
|
|
1619
|
+
// As above, Pokémon Showdown uses damage [of] but its not relevant
|
|
1620
|
+
if (amount > 0) try options.log.damage(.{ ident, stored, .LeechSeed });
|
|
1551
1621
|
|
|
1552
1622
|
const before = foe_stored.hp;
|
|
1553
1623
|
// Uncapped damage is added back to the foe
|
|
1554
1624
|
foe_stored.hp = @min(foe_stored.hp + damage, foe_stored.stats.hp);
|
|
1555
1625
|
// Pokémon Showdown uses the less specific heal here instead of drain... because reasons?
|
|
1556
|
-
if (foe_stored.hp > before) try options.log.heal(foe_ident, foe_stored, .Silent);
|
|
1626
|
+
if (foe_stored.hp > before) try options.log.heal(.{ foe_ident, foe_stored, .Silent });
|
|
1557
1627
|
}
|
|
1558
1628
|
}
|
|
1559
1629
|
|
|
@@ -1563,19 +1633,19 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1563
1633
|
|
|
1564
1634
|
battle.turn += 1;
|
|
1565
1635
|
|
|
1566
|
-
if (
|
|
1567
|
-
try options.log.tie();
|
|
1636
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1637
|
+
try options.log.tie(.{});
|
|
1568
1638
|
return Result.Tie;
|
|
1569
1639
|
}
|
|
1570
1640
|
|
|
1571
|
-
if (
|
|
1572
|
-
try options.log.tie();
|
|
1641
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1642
|
+
try options.log.tie(.{});
|
|
1573
1643
|
return Result.Tie;
|
|
1574
1644
|
} else if (battle.turn >= 65535) {
|
|
1575
1645
|
return Result.Error;
|
|
1576
1646
|
}
|
|
1577
1647
|
|
|
1578
|
-
try options.log.turn(battle.turn);
|
|
1648
|
+
try options.log.turn(.{battle.turn});
|
|
1579
1649
|
|
|
1580
1650
|
return Result.Default;
|
|
1581
1651
|
}
|
|
@@ -1645,7 +1715,7 @@ fn disabled(side: *Side, ident: ID, options: anytype) !bool {
|
|
|
1645
1715
|
side.last_selected_move;
|
|
1646
1716
|
if (m != .None and m == last) {
|
|
1647
1717
|
side.active.volatiles.Charging = false;
|
|
1648
|
-
try options.log.
|
|
1718
|
+
try options.log.cant(.{ ident, .Disable, last });
|
|
1649
1719
|
return true;
|
|
1650
1720
|
}
|
|
1651
1721
|
}
|
|
@@ -1659,17 +1729,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1659
1729
|
}
|
|
1660
1730
|
}
|
|
1661
1731
|
|
|
1662
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1663
|
-
var volatiles = &active.volatiles;
|
|
1664
|
-
assert(volatiles.Thrashing);
|
|
1665
|
-
if (volatiles.attacks > 0) return false;
|
|
1666
|
-
|
|
1667
|
-
volatiles.Thrashing = false;
|
|
1668
|
-
volatiles.Confusion = true;
|
|
1669
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1670
|
-
return true;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
1732
|
fn onBegin(
|
|
1674
1733
|
battle: anytype,
|
|
1675
1734
|
player: Player,
|
|
@@ -1678,7 +1737,6 @@ fn onBegin(
|
|
|
1678
1737
|
residual: *bool,
|
|
1679
1738
|
options: anytype,
|
|
1680
1739
|
) !void {
|
|
1681
|
-
assert(move.effect.onBegin());
|
|
1682
1740
|
return switch (move.effect) {
|
|
1683
1741
|
.Confusion => Effects.confusion(battle, player, move, options),
|
|
1684
1742
|
.Conversion => Effects.conversion(battle, player, options),
|
|
@@ -1701,7 +1759,6 @@ fn onBegin(
|
|
|
1701
1759
|
}
|
|
1702
1760
|
|
|
1703
1761
|
fn onEnd(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
1704
|
-
assert(move.effect.onEnd());
|
|
1705
1762
|
return switch (move.effect) {
|
|
1706
1763
|
.Bide => Effects.bide(battle, player, options),
|
|
1707
1764
|
// zig fmt: off
|
|
@@ -1723,7 +1780,6 @@ fn alwaysHappens(
|
|
|
1723
1780
|
residual: *bool,
|
|
1724
1781
|
options: anytype,
|
|
1725
1782
|
) !void {
|
|
1726
|
-
assert(move.effect.alwaysHappens());
|
|
1727
1783
|
return switch (move.effect) {
|
|
1728
1784
|
.DrainHP, .DreamEater => Effects.drainHP(battle, player, options),
|
|
1729
1785
|
.Explode => Effects.explode(battle, player),
|
|
@@ -1759,10 +1815,12 @@ pub const Effects = struct {
|
|
|
1759
1815
|
|
|
1760
1816
|
side.active.volatiles.Bide = true;
|
|
1761
1817
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1818
|
+
|
|
1762
1819
|
side.active.volatiles.state = 0;
|
|
1763
1820
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1821
|
+
options.chance.observe(.attacking, player, .started);
|
|
1764
1822
|
|
|
1765
|
-
try options.log.start(battle.active(player), .Bide);
|
|
1823
|
+
try options.log.start(.{ battle.active(player), .Bide });
|
|
1766
1824
|
}
|
|
1767
1825
|
|
|
1768
1826
|
fn burnChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1776,11 +1834,11 @@ pub const Effects = struct {
|
|
|
1776
1834
|
// GLITCH: Freeze top move selection desync can occur if thawed Pokémon is slower
|
|
1777
1835
|
if (Status.is(foe_stored.status, .FRZ)) {
|
|
1778
1836
|
assert(move.type == .Fire);
|
|
1779
|
-
try options.log.curestatus(
|
|
1837
|
+
try options.log.curestatus(.{
|
|
1780
1838
|
battle.active(player.foe()),
|
|
1781
1839
|
foe_stored.status,
|
|
1782
1840
|
.Message,
|
|
1783
|
-
);
|
|
1841
|
+
});
|
|
1784
1842
|
foe_stored.status = 0;
|
|
1785
1843
|
}
|
|
1786
1844
|
return;
|
|
@@ -1794,7 +1852,7 @@ pub const Effects = struct {
|
|
|
1794
1852
|
foe_stored.status = Status.init(.BRN);
|
|
1795
1853
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
1796
1854
|
|
|
1797
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
1855
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
1798
1856
|
}
|
|
1799
1857
|
|
|
1800
1858
|
fn charge(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1804,8 +1862,8 @@ pub const Effects = struct {
|
|
|
1804
1862
|
volatiles.Charging = true;
|
|
1805
1863
|
const move = side.last_selected_move;
|
|
1806
1864
|
if (move == .Fly or move == .Dig) volatiles.Invulnerable = true;
|
|
1807
|
-
try options.log.laststill();
|
|
1808
|
-
try options.log.prepare(battle.active(player), move);
|
|
1865
|
+
try options.log.laststill(.{});
|
|
1866
|
+
try options.log.prepare(.{ battle.active(player), move });
|
|
1809
1867
|
}
|
|
1810
1868
|
|
|
1811
1869
|
fn confusion(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1816,42 +1874,39 @@ pub const Effects = struct {
|
|
|
1816
1874
|
if (!try Rolls.confusionChance(battle, player, options)) return;
|
|
1817
1875
|
} else {
|
|
1818
1876
|
if (showdown) {
|
|
1819
|
-
if (!try checkHit(battle, player, move, options))
|
|
1820
|
-
|
|
1821
|
-
} else if (sub) {
|
|
1822
|
-
return options.log.fail(battle.active(player.foe()), .None);
|
|
1823
|
-
}
|
|
1877
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1878
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1824
1879
|
} else {
|
|
1825
|
-
if (sub) {
|
|
1826
|
-
|
|
1827
|
-
} else if (!try checkHit(battle, player, move, options)) {
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1880
|
+
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1881
|
+
if (!try checkHit(battle, player, move, options)) return;
|
|
1830
1882
|
}
|
|
1831
|
-
try options.chance.commit(player, .hit);
|
|
1832
1883
|
}
|
|
1833
1884
|
|
|
1834
1885
|
if (foe.active.volatiles.Confusion) return;
|
|
1835
1886
|
foe.active.volatiles.Confusion = true;
|
|
1836
|
-
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, false, options);
|
|
1837
1887
|
|
|
1838
|
-
|
|
1888
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1889
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1890
|
+
options.calc.confusion(player.foe());
|
|
1891
|
+
|
|
1892
|
+
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1839
1893
|
}
|
|
1840
1894
|
|
|
1841
1895
|
fn conversion(battle: anytype, player: Player, options: anytype) !void {
|
|
1842
1896
|
const foe = battle.foe(player);
|
|
1843
1897
|
|
|
1844
1898
|
if (foe.active.volatiles.Invulnerable) {
|
|
1845
|
-
try options.log.lastmiss();
|
|
1846
|
-
return options.log.miss(battle.active(player));
|
|
1899
|
+
try options.log.lastmiss(.{});
|
|
1900
|
+
return options.log.miss(.{battle.active(player)});
|
|
1847
1901
|
}
|
|
1848
1902
|
|
|
1849
1903
|
battle.side(player).active.types = foe.active.types;
|
|
1850
|
-
return options.log.
|
|
1904
|
+
return options.log.start(.{
|
|
1851
1905
|
battle.active(player),
|
|
1906
|
+
.TypeChange,
|
|
1852
1907
|
foe.active.types,
|
|
1853
1908
|
battle.active(player.foe()),
|
|
1854
|
-
);
|
|
1909
|
+
});
|
|
1855
1910
|
}
|
|
1856
1911
|
|
|
1857
1912
|
fn disable(battle: anytype, player: Player, move: Move.Data, options: anytype) !?Result {
|
|
@@ -1860,13 +1915,10 @@ pub const Effects = struct {
|
|
|
1860
1915
|
const foe_ident = battle.active(player.foe());
|
|
1861
1916
|
|
|
1862
1917
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1863
|
-
if (!showdown)
|
|
1864
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1865
|
-
try options.chance.commit(player, .hit);
|
|
1866
|
-
}
|
|
1918
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1867
1919
|
|
|
1868
1920
|
if (volatiles.disable_move != 0) {
|
|
1869
|
-
try options.log.fail(foe_ident, .None);
|
|
1921
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1870
1922
|
return null;
|
|
1871
1923
|
}
|
|
1872
1924
|
|
|
@@ -1881,26 +1933,27 @@ pub const Effects = struct {
|
|
|
1881
1933
|
|
|
1882
1934
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1883
1935
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1884
|
-
if (n == 0) {
|
|
1885
|
-
try options.log.fail(foe_ident, .None);
|
|
1936
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1937
|
+
try options.log.fail(.{ foe_ident, .None });
|
|
1886
1938
|
return null;
|
|
1887
1939
|
} else if (err) {
|
|
1888
1940
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1889
|
-
assert(!showdown);
|
|
1890
1941
|
return Result.Error;
|
|
1891
1942
|
}
|
|
1892
1943
|
|
|
1893
1944
|
volatiles.disable_move =
|
|
1894
1945
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1895
1946
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1947
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1896
1948
|
|
|
1897
1949
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1898
|
-
try options.log.
|
|
1950
|
+
try options.log.start(.{ foe_ident, .Disable, id });
|
|
1899
1951
|
return null;
|
|
1900
1952
|
}
|
|
1901
1953
|
|
|
1902
1954
|
fn drainHP(battle: anytype, player: Player, options: anytype) !void {
|
|
1903
1955
|
var stored = battle.side(player).stored();
|
|
1956
|
+
const ident = battle.active(player);
|
|
1904
1957
|
|
|
1905
1958
|
if (battle.last_damage == 0) {
|
|
1906
1959
|
assert(showdown);
|
|
@@ -1913,7 +1966,7 @@ pub const Effects = struct {
|
|
|
1913
1966
|
if (stored.hp == stored.stats.hp) return;
|
|
1914
1967
|
stored.hp = @min(stored.stats.hp, stored.hp + drain);
|
|
1915
1968
|
|
|
1916
|
-
try options.log.
|
|
1969
|
+
try options.log.heal(.{ ident, stored, .Drain, battle.active(player.foe()) });
|
|
1917
1970
|
}
|
|
1918
1971
|
|
|
1919
1972
|
fn explode(battle: anytype, player: Player) !void {
|
|
@@ -1945,7 +1998,7 @@ pub const Effects = struct {
|
|
|
1945
1998
|
if (side.active.volatiles.FocusEnergy) return;
|
|
1946
1999
|
side.active.volatiles.FocusEnergy = true;
|
|
1947
2000
|
|
|
1948
|
-
try options.log.start(battle.active(player), .FocusEnergy);
|
|
2001
|
+
try options.log.start(.{ battle.active(player), .FocusEnergy });
|
|
1949
2002
|
}
|
|
1950
2003
|
|
|
1951
2004
|
fn freezeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -1962,12 +2015,12 @@ pub const Effects = struct {
|
|
|
1962
2015
|
if (foe.active.types.includes(move.type)) return;
|
|
1963
2016
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1964
2017
|
// Freeze Clause Mod
|
|
1965
|
-
if (
|
|
2018
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1966
2019
|
|
|
1967
2020
|
foe_stored.status = Status.init(.FRZ);
|
|
1968
2021
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
1969
2022
|
|
|
1970
|
-
try options.log.status(foe_ident, foe_stored.status, .None);
|
|
2023
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .None });
|
|
1971
2024
|
}
|
|
1972
2025
|
|
|
1973
2026
|
fn haze(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -1975,7 +2028,7 @@ pub const Effects = struct {
|
|
|
1975
2028
|
var side = battle.side(player);
|
|
1976
2029
|
var foe = battle.foe(player);
|
|
1977
2030
|
|
|
1978
|
-
|
|
2031
|
+
const side_stored = side.stored();
|
|
1979
2032
|
var foe_stored = foe.stored();
|
|
1980
2033
|
|
|
1981
2034
|
const player_ident = battle.active(player);
|
|
@@ -1987,8 +2040,8 @@ pub const Effects = struct {
|
|
|
1987
2040
|
side.active.stats = side_stored.stats;
|
|
1988
2041
|
foe.active.stats = foe_stored.stats;
|
|
1989
2042
|
|
|
1990
|
-
try log.activate(player_ident, .Haze);
|
|
1991
|
-
try log.clearallboost();
|
|
2043
|
+
try log.activate(.{ player_ident, .Haze });
|
|
2044
|
+
try log.clearallboost(.{});
|
|
1992
2045
|
|
|
1993
2046
|
// Pokémon Showdown clears P1 then P2 instead of status -> side -> foe
|
|
1994
2047
|
if (showdown) {
|
|
@@ -1996,24 +2049,26 @@ pub const Effects = struct {
|
|
|
1996
2049
|
const p: Player = @enumFromInt(i);
|
|
1997
2050
|
// Pokémon Showdown incorrectly does not prevent sleep/freeze from moving
|
|
1998
2051
|
if (p != player and Status.any(s.stored().status)) {
|
|
1999
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2052
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2053
|
s.stored().status = 0;
|
|
2054
|
+
options.chance.observe(.sleep, p, .None);
|
|
2001
2055
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
2002
2056
|
s.stored().status = Status.init(.PSN);
|
|
2003
|
-
try log.status(battle.active(p), s.stored().status, .None);
|
|
2057
|
+
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
2004
2058
|
}
|
|
2005
|
-
try clearVolatiles(battle, p,
|
|
2059
|
+
try clearVolatiles(battle, p, options);
|
|
2006
2060
|
}
|
|
2007
2061
|
} else {
|
|
2008
2062
|
if (Status.any(foe_stored.status)) {
|
|
2009
2063
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
2010
2064
|
foe.last_selected_move = .SKIP_TURN;
|
|
2065
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
2011
2066
|
}
|
|
2012
|
-
try log.curestatus(foe_ident, foe_stored.status, .Silent);
|
|
2067
|
+
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2013
2068
|
foe_stored.status = 0;
|
|
2014
2069
|
}
|
|
2015
|
-
try clearVolatiles(battle, player,
|
|
2016
|
-
try clearVolatiles(battle, player.foe(),
|
|
2070
|
+
try clearVolatiles(battle, player, options);
|
|
2071
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2017
2072
|
}
|
|
2018
2073
|
}
|
|
2019
2074
|
|
|
@@ -2025,7 +2080,7 @@ pub const Effects = struct {
|
|
|
2025
2080
|
// GLITCH: HP recovery move failure glitches
|
|
2026
2081
|
const delta = stored.stats.hp - stored.hp;
|
|
2027
2082
|
if (delta == 0 or (delta & 255 == 255 and stored.hp % 256 != 0)) {
|
|
2028
|
-
return options.log.fail(ident, .None);
|
|
2083
|
+
return options.log.fail(.{ ident, .None });
|
|
2029
2084
|
}
|
|
2030
2085
|
|
|
2031
2086
|
const rest = side.last_selected_move == .Rest;
|
|
@@ -2033,17 +2088,17 @@ pub const Effects = struct {
|
|
|
2033
2088
|
// Adding the sleep status runs the sleep condition handler to roll duration
|
|
2034
2089
|
if (showdown) battle.rng.advance(1);
|
|
2035
2090
|
stored.status = Status.slf(2);
|
|
2036
|
-
try options.log.
|
|
2091
|
+
try options.log.status(.{ ident, stored.status, .From, Move.Rest });
|
|
2037
2092
|
stored.hp = stored.stats.hp;
|
|
2038
2093
|
} else {
|
|
2039
2094
|
stored.hp = @min(stored.stats.hp, stored.hp + (stored.stats.hp / 2));
|
|
2040
2095
|
}
|
|
2041
|
-
try options.log.heal(ident, stored, if (rest) Heal.Silent else Heal.None);
|
|
2096
|
+
try options.log.heal(.{ ident, stored, if (rest) Heal.Silent else Heal.None });
|
|
2042
2097
|
}
|
|
2043
2098
|
|
|
2044
2099
|
fn hyperBeam(battle: anytype, player: Player, options: anytype) !void {
|
|
2045
2100
|
battle.side(player).active.volatiles.Recharging = true;
|
|
2046
|
-
try options.log.mustrecharge(battle.active(player));
|
|
2101
|
+
try options.log.mustrecharge(.{battle.active(player)});
|
|
2047
2102
|
}
|
|
2048
2103
|
|
|
2049
2104
|
fn leechSeed(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2052,33 +2107,31 @@ pub const Effects = struct {
|
|
|
2052
2107
|
if (showdown) {
|
|
2053
2108
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2054
2109
|
if (!foe.active.volatiles.Invulnerable and foe.active.types.includes(.Grass)) {
|
|
2055
|
-
return options.log.immune(battle.active(player.foe()), .None);
|
|
2110
|
+
return options.log.immune(.{ battle.active(player.foe()), .None });
|
|
2056
2111
|
}
|
|
2057
2112
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2058
2113
|
if (foe.active.volatiles.LeechSeed) return;
|
|
2059
2114
|
} else {
|
|
2060
2115
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2061
2116
|
if (foe.active.types.includes(.Grass) or foe.active.volatiles.LeechSeed) {
|
|
2062
|
-
try options.log.lastmiss();
|
|
2063
|
-
return options.log.miss(battle.active(player));
|
|
2117
|
+
try options.log.lastmiss(.{});
|
|
2118
|
+
return options.log.miss(.{battle.active(player)});
|
|
2064
2119
|
}
|
|
2065
2120
|
}
|
|
2066
2121
|
|
|
2067
|
-
try options.chance.commit(player, .hit);
|
|
2068
2122
|
foe.active.volatiles.LeechSeed = true;
|
|
2069
2123
|
|
|
2070
|
-
try options.log.start(battle.active(player.foe()), .LeechSeed);
|
|
2124
|
+
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
2071
2125
|
}
|
|
2072
2126
|
|
|
2073
2127
|
fn lightScreen(battle: anytype, player: Player, options: anytype) !void {
|
|
2074
2128
|
var side = battle.side(player);
|
|
2129
|
+
const ident = battle.active(player);
|
|
2075
2130
|
|
|
2076
|
-
if (side.active.volatiles.LightScreen) {
|
|
2077
|
-
return options.log.fail(battle.active(player), .None);
|
|
2078
|
-
}
|
|
2131
|
+
if (side.active.volatiles.LightScreen) return options.log.fail(.{ ident, .None });
|
|
2079
2132
|
side.active.volatiles.LightScreen = true;
|
|
2080
2133
|
|
|
2081
|
-
try options.log.start(
|
|
2134
|
+
try options.log.start(.{ ident, .LightScreen });
|
|
2082
2135
|
}
|
|
2083
2136
|
|
|
2084
2137
|
fn mimic(battle: anytype, player: Player, move: Move.Data, mslot: u8, options: anytype) !void {
|
|
@@ -2106,17 +2159,15 @@ pub const Effects = struct {
|
|
|
2106
2159
|
if (!has_mimic) {
|
|
2107
2160
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2108
2161
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2109
|
-
|
|
2110
|
-
return options.log.fail(battle.active(player.foe()), .None);
|
|
2162
|
+
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2111
2163
|
}
|
|
2112
2164
|
}
|
|
2113
2165
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2114
|
-
try options.chance.commit(player, .hit);
|
|
2115
2166
|
|
|
2116
2167
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2117
2168
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
2118
2169
|
|
|
2119
|
-
try options.log.
|
|
2170
|
+
try options.log.start(.{ battle.active(player), .Mimic, side.active.move(oslot).id });
|
|
2120
2171
|
}
|
|
2121
2172
|
|
|
2122
2173
|
fn mist(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2125,7 +2176,7 @@ pub const Effects = struct {
|
|
|
2125
2176
|
if (side.active.volatiles.Mist) return;
|
|
2126
2177
|
side.active.volatiles.Mist = true;
|
|
2127
2178
|
|
|
2128
|
-
try options.log.start(battle.active(player), .Mist);
|
|
2179
|
+
try options.log.start(.{ battle.active(player), .Mist });
|
|
2129
2180
|
}
|
|
2130
2181
|
|
|
2131
2182
|
fn multiHit(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2151,25 +2202,25 @@ pub const Effects = struct {
|
|
|
2151
2202
|
|
|
2152
2203
|
if (showdown) {
|
|
2153
2204
|
// Invulnerability trumps type immunity on Pokémon Showdown
|
|
2154
|
-
if (immune and !foe.active.volatiles.Invulnerable)
|
|
2155
|
-
|
|
2205
|
+
if (immune and !foe.active.volatiles.Invulnerable) {
|
|
2206
|
+
return log.immune(.{ foe_ident, .None });
|
|
2207
|
+
} else if (!try checkHit(battle, player, move, options)) return;
|
|
2156
2208
|
}
|
|
2157
2209
|
if (Status.any(foe_stored.status)) {
|
|
2158
|
-
return log.fail(
|
|
2210
|
+
return log.fail(.{
|
|
2159
2211
|
foe_ident,
|
|
2160
|
-
if (Status.is(foe_stored.status, .PAR)) .Paralysis else .None,
|
|
2161
|
-
);
|
|
2212
|
+
if (Status.is(foe_stored.status, .PAR)) Fail.Paralysis else Fail.None,
|
|
2213
|
+
});
|
|
2162
2214
|
}
|
|
2163
2215
|
if (!showdown) {
|
|
2164
|
-
if (immune) return log.immune(foe_ident, .None);
|
|
2216
|
+
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2165
2217
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2166
2218
|
}
|
|
2167
|
-
try options.chance.commit(player, .hit);
|
|
2168
2219
|
|
|
2169
2220
|
foe_stored.status = Status.init(.PAR);
|
|
2170
2221
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2171
2222
|
|
|
2172
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2223
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2173
2224
|
}
|
|
2174
2225
|
|
|
2175
2226
|
fn paralyzeChance(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2191,12 +2242,12 @@ pub const Effects = struct {
|
|
|
2191
2242
|
foe_stored.status = Status.init(.PAR);
|
|
2192
2243
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
2193
2244
|
|
|
2194
|
-
try options.log.status(battle.active(player.foe()), foe_stored.status, .None);
|
|
2245
|
+
try options.log.status(.{ battle.active(player.foe()), foe_stored.status, .None });
|
|
2195
2246
|
}
|
|
2196
2247
|
|
|
2197
2248
|
fn payDay(battle: anytype, player: Player, options: anytype) !void {
|
|
2198
2249
|
if (!showdown or !battle.foe(player).active.volatiles.Substitute) {
|
|
2199
|
-
try options.log.fieldactivate();
|
|
2250
|
+
try options.log.fieldactivate(.{});
|
|
2200
2251
|
}
|
|
2201
2252
|
}
|
|
2202
2253
|
|
|
@@ -2211,28 +2262,27 @@ pub const Effects = struct {
|
|
|
2211
2262
|
return;
|
|
2212
2263
|
} else if (foe.active.volatiles.Substitute) {
|
|
2213
2264
|
if (move.effect != .Poison) return;
|
|
2214
|
-
return log.fail(foe_ident, .None);
|
|
2265
|
+
return log.fail(.{ foe_ident, .None });
|
|
2215
2266
|
} else if (Status.any(foe_stored.status)) {
|
|
2216
2267
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2217
2268
|
// Pokémon Showdown considers Toxic to be a status even in Generation I and so
|
|
2218
2269
|
// will not include a fail reason for Toxic vs. Poison or vice-versa...
|
|
2219
|
-
return log.fail(foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2270
|
+
return log.fail(.{ foe_ident, if (Status.is(foe_stored.status, .PSN))
|
|
2220
2271
|
if (!showdown)
|
|
2221
|
-
.Poison
|
|
2272
|
+
Fail.Poison
|
|
2222
2273
|
else if (toxic == (foe_stored.status == Status.TOX))
|
|
2223
|
-
if (toxic) .Toxic else .Poison
|
|
2274
|
+
if (toxic) Fail.Toxic else Fail.Poison
|
|
2224
2275
|
else
|
|
2225
|
-
.None
|
|
2276
|
+
Fail.None
|
|
2226
2277
|
else
|
|
2227
|
-
.None);
|
|
2278
|
+
Fail.None });
|
|
2228
2279
|
} else if (foe.active.types.includes(.Poison)) {
|
|
2229
2280
|
if (move.effect != .Poison) return if (showdown) battle.rng.advance(1);
|
|
2230
|
-
return log.immune(foe_ident, .None);
|
|
2281
|
+
return log.immune(.{ foe_ident, .None });
|
|
2231
2282
|
}
|
|
2232
2283
|
|
|
2233
2284
|
if (move.effect == .Poison) {
|
|
2234
2285
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2235
|
-
try options.chance.commit(player, .hit);
|
|
2236
2286
|
} else {
|
|
2237
2287
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2238
2288
|
return;
|
|
@@ -2246,7 +2296,7 @@ pub const Effects = struct {
|
|
|
2246
2296
|
foe.active.volatiles.toxic = 0;
|
|
2247
2297
|
}
|
|
2248
2298
|
|
|
2249
|
-
try log.status(foe_ident, foe_stored.status, .None);
|
|
2299
|
+
try log.status(.{ foe_ident, foe_stored.status, .None });
|
|
2250
2300
|
}
|
|
2251
2301
|
|
|
2252
2302
|
fn rage(battle: anytype, player: Player) !void {
|
|
@@ -2266,22 +2316,23 @@ pub const Effects = struct {
|
|
|
2266
2316
|
@as(u8, if (side.last_selected_move == .Struggle) 2 else 4), 1));
|
|
2267
2317
|
stored.hp = @intCast(@max(@as(i16, @intCast(stored.hp)) - damage, 0));
|
|
2268
2318
|
|
|
2269
|
-
try options.log.
|
|
2319
|
+
try options.log.damage(.{
|
|
2270
2320
|
battle.active(player),
|
|
2271
2321
|
stored,
|
|
2272
2322
|
.RecoilOf,
|
|
2273
2323
|
battle.active(player.foe()),
|
|
2274
|
-
);
|
|
2324
|
+
});
|
|
2275
2325
|
if (showdown and stored.hp == 0) residual.* = false;
|
|
2276
2326
|
}
|
|
2277
2327
|
|
|
2278
2328
|
fn reflect(battle: anytype, player: Player, options: anytype) !void {
|
|
2279
2329
|
var side = battle.side(player);
|
|
2330
|
+
const ident = battle.active(player);
|
|
2280
2331
|
|
|
2281
|
-
if (side.active.volatiles.Reflect) return options.log.fail(
|
|
2332
|
+
if (side.active.volatiles.Reflect) return options.log.fail(.{ ident, .None });
|
|
2282
2333
|
side.active.volatiles.Reflect = true;
|
|
2283
2334
|
|
|
2284
|
-
try options.log.start(
|
|
2335
|
+
try options.log.start(.{ ident, .Reflect });
|
|
2285
2336
|
}
|
|
2286
2337
|
|
|
2287
2338
|
fn sleep(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2295,18 +2346,17 @@ pub const Effects = struct {
|
|
|
2295
2346
|
// the Sleep Clause Mod might activate, causing us to not actually bypass
|
|
2296
2347
|
} else {
|
|
2297
2348
|
if (Status.any(foe_stored.status)) {
|
|
2298
|
-
return options.log.fail(
|
|
2349
|
+
return options.log.fail(.{
|
|
2299
2350
|
foe_ident,
|
|
2300
|
-
if (Status.is(foe_stored.status, .SLP)) .Sleep else .None,
|
|
2301
|
-
);
|
|
2351
|
+
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2352
|
+
});
|
|
2302
2353
|
}
|
|
2303
|
-
// If
|
|
2354
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2304
2355
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2305
|
-
try options.chance.commit(player, .hit);
|
|
2306
2356
|
}
|
|
2307
2357
|
|
|
2308
2358
|
// Sleep Clause Mod
|
|
2309
|
-
if (
|
|
2359
|
+
if (pkmn.options.mod) {
|
|
2310
2360
|
for (foe.pokemon) |p| {
|
|
2311
2361
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2312
2362
|
}
|
|
@@ -2314,18 +2364,20 @@ pub const Effects = struct {
|
|
|
2314
2364
|
foe.active.volatiles.Recharging = false;
|
|
2315
2365
|
|
|
2316
2366
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2367
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2368
|
+
|
|
2317
2369
|
const last = battle.side(player).last_selected_move;
|
|
2318
|
-
try options.log.
|
|
2370
|
+
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2319
2371
|
}
|
|
2320
2372
|
|
|
2321
2373
|
fn splash(battle: anytype, player: Player, options: anytype) !void {
|
|
2322
|
-
try options.log.activate(battle.active(player), .Splash);
|
|
2374
|
+
try options.log.activate(.{ battle.active(player), .Splash });
|
|
2323
2375
|
}
|
|
2324
2376
|
|
|
2325
2377
|
fn substitute(battle: anytype, player: Player, residual: *bool, options: anytype) !void {
|
|
2326
2378
|
var side = battle.side(player);
|
|
2327
2379
|
if (side.active.volatiles.Substitute) {
|
|
2328
|
-
return options.log.fail(battle.active(player), .Substitute);
|
|
2380
|
+
return options.log.fail(.{ battle.active(player), .Substitute });
|
|
2329
2381
|
}
|
|
2330
2382
|
|
|
2331
2383
|
assert(side.stored().stats.hp <= 1023);
|
|
@@ -2336,15 +2388,17 @@ pub const Effects = struct {
|
|
|
2336
2388
|
// the target's HP is exactly divisible by 4 (here we're using an inlined divCeil routine to
|
|
2337
2389
|
// avoid having to convert to floating point)
|
|
2338
2390
|
const required_hp = if (showdown) @divFloor(side.stored().stats.hp - 1, 4) + 1 else hp;
|
|
2339
|
-
if (side.stored().hp < required_hp)
|
|
2391
|
+
if (side.stored().hp < required_hp) {
|
|
2392
|
+
return options.log.fail(.{ battle.active(player), .Weak });
|
|
2393
|
+
}
|
|
2340
2394
|
|
|
2341
2395
|
// GLITCH: can leave the user with 0 HP (faints later) because didn't check '<=' above
|
|
2342
2396
|
side.stored().hp -= hp;
|
|
2343
2397
|
side.active.volatiles.substitute = hp + 1;
|
|
2344
2398
|
side.active.volatiles.Substitute = true;
|
|
2345
|
-
try options.log.start(battle.active(player), .Substitute);
|
|
2399
|
+
try options.log.start(.{ battle.active(player), .Substitute });
|
|
2346
2400
|
if (hp > 0) {
|
|
2347
|
-
try options.log.damage(battle.active(player), side.stored(), .None);
|
|
2401
|
+
try options.log.damage(.{ battle.active(player), side.stored(), .None });
|
|
2348
2402
|
if (showdown and side.stored().hp == 0) residual.* = false;
|
|
2349
2403
|
}
|
|
2350
2404
|
}
|
|
@@ -2353,13 +2407,11 @@ pub const Effects = struct {
|
|
|
2353
2407
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2354
2408
|
if (showdown) {
|
|
2355
2409
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2356
|
-
|
|
2357
|
-
try options.chance.commit(player, .hit);
|
|
2358
|
-
}
|
|
2410
|
+
_ = try checkHit(battle, player, move, options);
|
|
2359
2411
|
battle.last_damage = 0;
|
|
2360
2412
|
} else {
|
|
2361
|
-
try options.log.fail(battle.active(player), .None);
|
|
2362
|
-
try options.log.laststill();
|
|
2413
|
+
try options.log.fail(.{ battle.active(player), .None });
|
|
2414
|
+
try options.log.laststill(.{});
|
|
2363
2415
|
}
|
|
2364
2416
|
}
|
|
2365
2417
|
|
|
@@ -2370,6 +2422,8 @@ pub const Effects = struct {
|
|
|
2370
2422
|
|
|
2371
2423
|
volatiles.Thrashing = true;
|
|
2372
2424
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2425
|
+
|
|
2426
|
+
options.chance.observe(.attacking, player, .started);
|
|
2373
2427
|
}
|
|
2374
2428
|
|
|
2375
2429
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2398,7 +2452,7 @@ pub const Effects = struct {
|
|
|
2398
2452
|
side.active.moves[i].pp = if (m.id != .None) 5 else 0;
|
|
2399
2453
|
}
|
|
2400
2454
|
|
|
2401
|
-
try options.log.transform(battle.active(player), foe_ident);
|
|
2455
|
+
try options.log.transform(.{ battle.active(player), foe_ident });
|
|
2402
2456
|
}
|
|
2403
2457
|
|
|
2404
2458
|
fn binding(battle: anytype, player: Player, rewrap: bool, options: anytype) !void {
|
|
@@ -2408,13 +2462,14 @@ pub const Effects = struct {
|
|
|
2408
2462
|
if (side.active.volatiles.Binding) return;
|
|
2409
2463
|
side.active.volatiles.Binding = true;
|
|
2410
2464
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2411
|
-
// (Pokémon Showdown
|
|
2465
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2412
2466
|
if (!showdown) {
|
|
2413
2467
|
foe.active.volatiles.Recharging = false;
|
|
2414
2468
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2415
2469
|
|
|
2416
2470
|
side.active.volatiles.attacks =
|
|
2417
2471
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2472
|
+
options.chance.observe(.binding, player, .started);
|
|
2418
2473
|
}
|
|
2419
2474
|
|
|
2420
2475
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2428,87 +2483,87 @@ pub const Effects = struct {
|
|
|
2428
2483
|
switch (move.effect) {
|
|
2429
2484
|
.AttackUp1, .AttackUp2, .Rage => {
|
|
2430
2485
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2431
|
-
if (boosts.atk == 6) return log.fail(ident, .None);
|
|
2486
|
+
if (boosts.atk == 6) return log.fail(.{ ident, .None });
|
|
2432
2487
|
const n: u2 = if (move.effect == .AttackUp2) 2 else 1;
|
|
2433
2488
|
boosts.atk = @as(i4, @intCast(@min(6, @as(i8, boosts.atk) + n)));
|
|
2434
|
-
const reason = if (move.effect == .Rage)
|
|
2489
|
+
const reason: Boost = if (move.effect == .Rage) .Rage else .Attack;
|
|
2435
2490
|
if (stats.atk == MAX_STAT_VALUE) {
|
|
2436
2491
|
boosts.atk -= 1;
|
|
2437
2492
|
if (showdown) {
|
|
2438
|
-
try log.boost(ident, reason, n);
|
|
2439
|
-
try log.boost(ident,
|
|
2493
|
+
try log.boost(.{ ident, reason, n });
|
|
2494
|
+
try log.boost(.{ ident, .Attack, -1 });
|
|
2440
2495
|
if (move.effect == .Rage) return;
|
|
2441
2496
|
}
|
|
2442
|
-
return log.fail(ident, .None);
|
|
2497
|
+
return log.fail(.{ ident, .None });
|
|
2443
2498
|
}
|
|
2444
|
-
|
|
2499
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2445
2500
|
const stat = unmodifiedStats(battle, side).atk;
|
|
2446
2501
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2447
|
-
try log.boost(ident, reason, n);
|
|
2502
|
+
try log.boost(.{ ident, reason, n });
|
|
2448
2503
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2449
2504
|
if (showdown and move.effect == .Rage) return;
|
|
2450
2505
|
},
|
|
2451
2506
|
.DefenseUp1, .DefenseUp2 => {
|
|
2452
2507
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2453
|
-
if (boosts.def == 6) return log.fail(ident, .None);
|
|
2508
|
+
if (boosts.def == 6) return log.fail(.{ ident, .None });
|
|
2454
2509
|
const n: u2 = if (move.effect == .DefenseUp2) 2 else 1;
|
|
2455
2510
|
boosts.def = @intCast(@min(6, @as(i8, boosts.def) + n));
|
|
2456
2511
|
if (stats.def == MAX_STAT_VALUE) {
|
|
2457
2512
|
boosts.def -= 1;
|
|
2458
2513
|
if (showdown) {
|
|
2459
|
-
try log.boost(ident, .Defense, n);
|
|
2460
|
-
try log.boost(ident, .Defense, -1);
|
|
2514
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2515
|
+
try log.boost(.{ ident, .Defense, -1 });
|
|
2461
2516
|
}
|
|
2462
|
-
return log.fail(ident, .None);
|
|
2517
|
+
return log.fail(.{ ident, .None });
|
|
2463
2518
|
}
|
|
2464
|
-
|
|
2519
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2465
2520
|
const stat = unmodifiedStats(battle, side).def;
|
|
2466
2521
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2467
|
-
try log.boost(ident, .Defense, n);
|
|
2522
|
+
try log.boost(.{ ident, .Defense, n });
|
|
2468
2523
|
},
|
|
2469
2524
|
.SpeedUp2 => {
|
|
2470
2525
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2471
|
-
if (boosts.spe == 6) return log.fail(ident, .None);
|
|
2526
|
+
if (boosts.spe == 6) return log.fail(.{ ident, .None });
|
|
2472
2527
|
boosts.spe = @intCast(@min(6, @as(i8, boosts.spe) + 2));
|
|
2473
2528
|
if (stats.spe == MAX_STAT_VALUE) {
|
|
2474
2529
|
boosts.spe -= 1;
|
|
2475
2530
|
if (showdown) {
|
|
2476
|
-
try log.boost(ident, .Speed, 2);
|
|
2477
|
-
try log.boost(ident, .Speed, -1);
|
|
2531
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2532
|
+
try log.boost(.{ ident, .Speed, -1 });
|
|
2478
2533
|
}
|
|
2479
|
-
return log.fail(ident, .None);
|
|
2534
|
+
return log.fail(.{ ident, .None });
|
|
2480
2535
|
}
|
|
2481
|
-
|
|
2536
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2482
2537
|
const stat = unmodifiedStats(battle, side).spe;
|
|
2483
2538
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2484
|
-
try log.boost(ident, .Speed, 2);
|
|
2539
|
+
try log.boost(.{ ident, .Speed, 2 });
|
|
2485
2540
|
},
|
|
2486
2541
|
.SpecialUp1, .SpecialUp2 => {
|
|
2487
2542
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2488
|
-
if (boosts.spc == 6) return log.fail(ident, .None);
|
|
2543
|
+
if (boosts.spc == 6) return log.fail(.{ ident, .None });
|
|
2489
2544
|
const n: u2 = if (move.effect == .SpecialUp2) 2 else 1;
|
|
2490
2545
|
boosts.spc = @intCast(@min(6, @as(i8, boosts.spc) + n));
|
|
2491
2546
|
if (stats.spc == MAX_STAT_VALUE) {
|
|
2492
2547
|
boosts.spc -= 1;
|
|
2493
2548
|
if (showdown) {
|
|
2494
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2495
|
-
try log.boost(ident, .SpecialAttack, -1);
|
|
2496
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2497
|
-
try log.boost(ident, .SpecialDefense, -1);
|
|
2549
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2550
|
+
try log.boost(.{ ident, .SpecialAttack, -1 });
|
|
2551
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2552
|
+
try log.boost(.{ ident, .SpecialDefense, -1 });
|
|
2498
2553
|
}
|
|
2499
|
-
return log.fail(ident, .None);
|
|
2554
|
+
return log.fail(.{ ident, .None });
|
|
2500
2555
|
}
|
|
2501
|
-
|
|
2556
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2502
2557
|
const stat = unmodifiedStats(battle, side).spc;
|
|
2503
2558
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2504
|
-
try log.boost(ident, .SpecialAttack, n);
|
|
2505
|
-
try log.boost(ident, .SpecialDefense, n);
|
|
2559
|
+
try log.boost(.{ ident, .SpecialAttack, n });
|
|
2560
|
+
try log.boost(.{ ident, .SpecialDefense, n });
|
|
2506
2561
|
},
|
|
2507
2562
|
.EvasionUp1 => {
|
|
2508
2563
|
assert(boosts.evasion >= -6 and boosts.evasion <= 6);
|
|
2509
|
-
if (boosts.evasion == 6) return log.fail(ident, .None);
|
|
2564
|
+
if (boosts.evasion == 6) return log.fail(.{ ident, .None });
|
|
2510
2565
|
boosts.evasion = @intCast(@min(6, @as(i8, boosts.evasion) + 1));
|
|
2511
|
-
try log.boost(ident, .Evasion, 1);
|
|
2566
|
+
try log.boost(.{ ident, .Evasion, 1 });
|
|
2512
2567
|
},
|
|
2513
2568
|
else => unreachable,
|
|
2514
2569
|
}
|
|
@@ -2523,7 +2578,7 @@ pub const Effects = struct {
|
|
|
2523
2578
|
const foe_ident = battle.active(player.foe());
|
|
2524
2579
|
const secondary = move.effect.isStatDownChance();
|
|
2525
2580
|
|
|
2526
|
-
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(foe_ident, .None);
|
|
2581
|
+
if (foe.active.volatiles.Substitute) return if (!secondary) log.fail(.{ foe_ident, .None });
|
|
2527
2582
|
|
|
2528
2583
|
if (secondary) {
|
|
2529
2584
|
const proc = try Rolls.unboost(battle, player, options);
|
|
@@ -2531,7 +2586,6 @@ pub const Effects = struct {
|
|
|
2531
2586
|
} else {
|
|
2532
2587
|
// checkHit already checks for Invulnerable
|
|
2533
2588
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2534
|
-
try options.chance.commit(player, .hit);
|
|
2535
2589
|
}
|
|
2536
2590
|
|
|
2537
2591
|
var stats = &foe.active.stats;
|
|
@@ -2541,82 +2595,82 @@ pub const Effects = struct {
|
|
|
2541
2595
|
switch (move.effect) {
|
|
2542
2596
|
.AttackDown1, .AttackDownChance => {
|
|
2543
2597
|
assert(boosts.atk >= -6 and boosts.atk <= 6);
|
|
2544
|
-
if (boosts.atk == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2598
|
+
if (boosts.atk == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2545
2599
|
boosts.atk = @intCast(@max(-6, @as(i8, boosts.atk) - 1));
|
|
2546
2600
|
if (stats.atk == 1) {
|
|
2547
2601
|
boosts.atk += 1;
|
|
2548
2602
|
if (showdown) {
|
|
2549
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2550
|
-
try log.boost(foe_ident, .Attack, 1);
|
|
2603
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2604
|
+
try log.boost(.{ foe_ident, .Attack, 1 });
|
|
2551
2605
|
}
|
|
2552
|
-
return log.fail(foe_ident, .None);
|
|
2606
|
+
return log.fail(.{ foe_ident, .None });
|
|
2553
2607
|
}
|
|
2554
|
-
|
|
2608
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.atk) + 6))];
|
|
2555
2609
|
const stat = unmodifiedStats(battle, foe).atk;
|
|
2556
2610
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2557
|
-
try log.boost(foe_ident, .Attack, -1);
|
|
2611
|
+
try log.boost(.{ foe_ident, .Attack, -1 });
|
|
2558
2612
|
},
|
|
2559
2613
|
.DefenseDown1, .DefenseDown2, .DefenseDownChance => {
|
|
2560
2614
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
2561
|
-
if (boosts.def == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2615
|
+
if (boosts.def == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2562
2616
|
const n: u2 = if (move.effect == .DefenseDown2) 2 else 1;
|
|
2563
2617
|
boosts.def = @intCast(@max(-6, @as(i8, boosts.def) - n));
|
|
2564
2618
|
if (stats.def == 1) {
|
|
2565
2619
|
boosts.def += 1;
|
|
2566
2620
|
if (showdown) {
|
|
2567
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2568
|
-
try log.boost(foe_ident, .Defense, 1);
|
|
2621
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2622
|
+
try log.boost(.{ foe_ident, .Defense, 1 });
|
|
2569
2623
|
}
|
|
2570
|
-
return log.fail(foe_ident, .None);
|
|
2624
|
+
return log.fail(.{ foe_ident, .None });
|
|
2571
2625
|
}
|
|
2572
|
-
|
|
2626
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.def) + 6))];
|
|
2573
2627
|
const stat = unmodifiedStats(battle, foe).def;
|
|
2574
2628
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2575
|
-
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2629
|
+
try log.boost(.{ foe_ident, .Defense, -@as(i8, n) });
|
|
2576
2630
|
},
|
|
2577
2631
|
.SpeedDown1, .SpeedDownChance => {
|
|
2578
2632
|
assert(boosts.spe >= -6 and boosts.spe <= 6);
|
|
2579
|
-
if (boosts.spe == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2633
|
+
if (boosts.spe == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2580
2634
|
boosts.spe = @intCast(@max(-6, @as(i8, boosts.spe) - 1));
|
|
2581
2635
|
if (stats.spe == 1) {
|
|
2582
2636
|
boosts.spe += 1;
|
|
2583
2637
|
if (showdown) {
|
|
2584
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2585
|
-
try log.boost(foe_ident, .Speed, 1);
|
|
2638
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2639
|
+
try log.boost(.{ foe_ident, .Speed, 1 });
|
|
2586
2640
|
}
|
|
2587
|
-
return log.fail(foe_ident, .None);
|
|
2641
|
+
return log.fail(.{ foe_ident, .None });
|
|
2588
2642
|
}
|
|
2589
|
-
|
|
2643
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spe) + 6))];
|
|
2590
2644
|
const stat = unmodifiedStats(battle, foe).spe;
|
|
2591
2645
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2592
|
-
try log.boost(foe_ident, .Speed, -1);
|
|
2646
|
+
try log.boost(.{ foe_ident, .Speed, -1 });
|
|
2593
2647
|
assert(boosts.spe >= -6);
|
|
2594
2648
|
},
|
|
2595
2649
|
.SpecialDownChance => {
|
|
2596
2650
|
assert(boosts.spc >= -6 and boosts.spc <= 6);
|
|
2597
|
-
if (boosts.spc == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2651
|
+
if (boosts.spc == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2598
2652
|
boosts.spc = @intCast(@max(-6, @as(i8, boosts.spc) - 1));
|
|
2599
2653
|
if (stats.spc == 1) {
|
|
2600
2654
|
boosts.spc += 1;
|
|
2601
2655
|
if (showdown) {
|
|
2602
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2603
|
-
try log.boost(foe_ident, .SpecialAttack, 1);
|
|
2604
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2605
|
-
try log.boost(foe_ident, .SpecialDefense, 1);
|
|
2656
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2657
|
+
try log.boost(.{ foe_ident, .SpecialAttack, 1 });
|
|
2658
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2659
|
+
try log.boost(.{ foe_ident, .SpecialDefense, 1 });
|
|
2606
2660
|
}
|
|
2607
|
-
return log.fail(foe_ident, .None);
|
|
2661
|
+
return log.fail(.{ foe_ident, .None });
|
|
2608
2662
|
}
|
|
2609
|
-
|
|
2663
|
+
const mod = BOOSTS[@as(u4, @intCast(@as(i8, boosts.spc) + 6))];
|
|
2610
2664
|
const stat = unmodifiedStats(battle, foe).spc;
|
|
2611
2665
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2612
|
-
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2613
|
-
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
2666
|
+
try log.boost(.{ foe_ident, .SpecialAttack, -1 });
|
|
2667
|
+
try log.boost(.{ foe_ident, .SpecialDefense, -1 });
|
|
2614
2668
|
},
|
|
2615
2669
|
.AccuracyDown1 => {
|
|
2616
2670
|
assert(boosts.accuracy >= -6 and boosts.accuracy <= 6);
|
|
2617
|
-
if (boosts.accuracy == -6) return if (fail) try log.fail(foe_ident, .None);
|
|
2671
|
+
if (boosts.accuracy == -6) return if (fail) try log.fail(.{ foe_ident, .None });
|
|
2618
2672
|
boosts.accuracy = @intCast(@max(-6, @as(i8, boosts.accuracy) - 1));
|
|
2619
|
-
try log.boost(foe_ident, .Accuracy, -1);
|
|
2673
|
+
try log.boost(.{ foe_ident, .Accuracy, -1 });
|
|
2620
2674
|
},
|
|
2621
2675
|
else => unreachable,
|
|
2622
2676
|
}
|
|
@@ -2645,35 +2699,35 @@ fn isForced(active: anytype) bool {
|
|
|
2645
2699
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2646
2700
|
}
|
|
2647
2701
|
|
|
2648
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2702
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2649
2703
|
var log = options.log;
|
|
2650
2704
|
var side = battle.side(who);
|
|
2651
2705
|
var volatiles = &side.active.volatiles;
|
|
2652
2706
|
const ident = battle.active(who);
|
|
2653
2707
|
|
|
2654
|
-
options.chance.clearDurations(who, clear);
|
|
2655
|
-
|
|
2656
2708
|
if (volatiles.disable_move != 0) {
|
|
2657
2709
|
volatiles.disable_move = 0;
|
|
2658
2710
|
volatiles.disable_duration = 0;
|
|
2659
|
-
|
|
2711
|
+
options.chance.observe(.disable, who, .None);
|
|
2712
|
+
try log.end(.{ ident, .DisableSilent });
|
|
2660
2713
|
}
|
|
2661
2714
|
if (volatiles.Confusion) {
|
|
2662
2715
|
// volatiles.confusion is left unchanged
|
|
2663
2716
|
volatiles.Confusion = false;
|
|
2664
|
-
|
|
2717
|
+
options.chance.observe(.confusion, who, .None);
|
|
2718
|
+
try log.end(.{ ident, .ConfusionSilent });
|
|
2665
2719
|
}
|
|
2666
2720
|
if (volatiles.Mist) {
|
|
2667
2721
|
volatiles.Mist = false;
|
|
2668
|
-
try log.end(ident, .MistSilent);
|
|
2722
|
+
try log.end(.{ ident, .MistSilent });
|
|
2669
2723
|
}
|
|
2670
2724
|
if (volatiles.FocusEnergy) {
|
|
2671
2725
|
volatiles.FocusEnergy = false;
|
|
2672
|
-
try log.end(ident, .FocusEnergySilent);
|
|
2726
|
+
try log.end(.{ ident, .FocusEnergySilent });
|
|
2673
2727
|
}
|
|
2674
2728
|
if (volatiles.LeechSeed) {
|
|
2675
2729
|
volatiles.LeechSeed = false;
|
|
2676
|
-
try log.end(ident, .LeechSeedSilent);
|
|
2730
|
+
try log.end(.{ ident, .LeechSeedSilent });
|
|
2677
2731
|
}
|
|
2678
2732
|
if (!showdown and volatiles.Toxic) {
|
|
2679
2733
|
volatiles.Toxic = false;
|
|
@@ -2681,24 +2735,39 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2681
2735
|
}
|
|
2682
2736
|
if (volatiles.LightScreen) {
|
|
2683
2737
|
volatiles.LightScreen = false;
|
|
2684
|
-
try log.end(ident, .LightScreenSilent);
|
|
2738
|
+
try log.end(.{ ident, .LightScreenSilent });
|
|
2685
2739
|
}
|
|
2686
2740
|
if (volatiles.Reflect) {
|
|
2687
2741
|
volatiles.Reflect = false;
|
|
2688
|
-
try log.end(ident, .ReflectSilent);
|
|
2742
|
+
try log.end(.{ ident, .ReflectSilent });
|
|
2689
2743
|
}
|
|
2690
2744
|
if (showdown and volatiles.Toxic) {
|
|
2691
2745
|
volatiles.Toxic = false;
|
|
2692
2746
|
// Pokémon Showdown erroneously clears the toxic counter
|
|
2693
2747
|
volatiles.toxic = 0;
|
|
2694
|
-
try log.end(ident, .ToxicSilent);
|
|
2748
|
+
try log.end(.{ ident, .ToxicSilent });
|
|
2695
2749
|
}
|
|
2696
2750
|
}
|
|
2697
2751
|
|
|
2752
|
+
fn decrement(
|
|
2753
|
+
comptime field: chance.Action.Field,
|
|
2754
|
+
player: Player,
|
|
2755
|
+
options: anytype,
|
|
2756
|
+
n: anytype,
|
|
2757
|
+
) @TypeOf(n) {
|
|
2758
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2759
|
+
.started, .ended => 0,
|
|
2760
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2761
|
+
else => unreachable,
|
|
2762
|
+
} else n - 1;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2698
2765
|
pub const Rolls = struct {
|
|
2699
2766
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2700
2767
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
2701
2768
|
player == .P1
|
|
2769
|
+
else if (@hasDecl(@TypeOf(battle.rng), "speedTie"))
|
|
2770
|
+
battle.rng.speedTie()
|
|
2702
2771
|
else if (showdown)
|
|
2703
2772
|
battle.rng.range(u8, 0, 2) == 0
|
|
2704
2773
|
else
|
|
@@ -2708,21 +2777,31 @@ pub const Rolls = struct {
|
|
|
2708
2777
|
return p1;
|
|
2709
2778
|
}
|
|
2710
2779
|
|
|
2711
|
-
fn criticalHit(
|
|
2780
|
+
fn criticalHit(
|
|
2781
|
+
battle: anytype,
|
|
2782
|
+
player: Player,
|
|
2783
|
+
rate: u8,
|
|
2784
|
+
spurious: bool,
|
|
2785
|
+
options: anytype,
|
|
2786
|
+
) !bool {
|
|
2712
2787
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2713
2788
|
val == .true
|
|
2789
|
+
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
2790
|
+
battle.rng.criticalHit(player, rate)
|
|
2714
2791
|
else if (showdown)
|
|
2715
2792
|
battle.rng.chance(u8, rate, 256)
|
|
2716
2793
|
else
|
|
2717
2794
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2718
2795
|
|
|
2719
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2796
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2720
2797
|
return crit;
|
|
2721
2798
|
}
|
|
2722
2799
|
|
|
2723
2800
|
fn damage(battle: anytype, player: Player, options: anytype) !u8 {
|
|
2724
2801
|
const roll = if (options.calc.overridden(player, .damage)) |val|
|
|
2725
2802
|
val
|
|
2803
|
+
else if (@hasDecl(@TypeOf(battle.rng), "damage"))
|
|
2804
|
+
battle.rng.damage(player)
|
|
2726
2805
|
else roll: {
|
|
2727
2806
|
if (showdown) break :roll battle.rng.range(u8, 217, 256);
|
|
2728
2807
|
while (true) {
|
|
@@ -2736,21 +2815,27 @@ pub const Rolls = struct {
|
|
|
2736
2815
|
return roll;
|
|
2737
2816
|
}
|
|
2738
2817
|
|
|
2739
|
-
|
|
2818
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2819
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2820
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2740
2821
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2741
2822
|
val == .true
|
|
2823
|
+
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2824
|
+
battle.rng.hit(player, acc)
|
|
2742
2825
|
else if (showdown)
|
|
2743
|
-
battle.rng.chance(
|
|
2826
|
+
battle.rng.chance(uN, acc, 256)
|
|
2744
2827
|
else
|
|
2745
|
-
battle.rng.next() <
|
|
2828
|
+
battle.rng.next() < acc;
|
|
2746
2829
|
|
|
2747
|
-
options.chance.hit(ok,
|
|
2830
|
+
try options.chance.hit(player, ok, acc);
|
|
2748
2831
|
return ok;
|
|
2749
2832
|
}
|
|
2750
2833
|
|
|
2751
2834
|
fn confused(battle: anytype, player: Player, options: anytype) !bool {
|
|
2752
2835
|
const cfz = if (options.calc.overridden(player, .confused)) |val|
|
|
2753
2836
|
val == .true
|
|
2837
|
+
else if (@hasDecl(@TypeOf(battle.rng), "confused"))
|
|
2838
|
+
battle.rng.confused(player)
|
|
2754
2839
|
else if (showdown)
|
|
2755
2840
|
!battle.rng.chance(u8, 128, 256)
|
|
2756
2841
|
else
|
|
@@ -2763,6 +2848,8 @@ pub const Rolls = struct {
|
|
|
2763
2848
|
fn paralyzed(battle: anytype, player: Player, options: anytype) !bool {
|
|
2764
2849
|
const par = if (options.calc.overridden(player, .paralyzed)) |val|
|
|
2765
2850
|
val == .true
|
|
2851
|
+
else if (@hasDecl(@TypeOf(battle.rng), "paralyzed"))
|
|
2852
|
+
battle.rng.paralyzed(player)
|
|
2766
2853
|
else if (showdown)
|
|
2767
2854
|
battle.rng.chance(u8, 63, 256)
|
|
2768
2855
|
else
|
|
@@ -2775,6 +2862,8 @@ pub const Rolls = struct {
|
|
|
2775
2862
|
fn confusionChance(battle: anytype, player: Player, options: anytype) !bool {
|
|
2776
2863
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2777
2864
|
val == .true
|
|
2865
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2866
|
+
battle.rng.secondaryChance(player, 25)
|
|
2778
2867
|
else if (showdown)
|
|
2779
2868
|
battle.rng.chance(u8, 25, 256)
|
|
2780
2869
|
else
|
|
@@ -2789,6 +2878,8 @@ pub const Rolls = struct {
|
|
|
2789
2878
|
|
|
2790
2879
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2791
2880
|
val == .true
|
|
2881
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2882
|
+
battle.rng.secondaryChance(player, rate)
|
|
2792
2883
|
else if (showdown)
|
|
2793
2884
|
battle.rng.chance(u8, rate, 256)
|
|
2794
2885
|
else
|
|
@@ -2803,6 +2894,8 @@ pub const Rolls = struct {
|
|
|
2803
2894
|
|
|
2804
2895
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2805
2896
|
val == .true
|
|
2897
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2898
|
+
battle.rng.secondaryChance(player, rate)
|
|
2806
2899
|
else if (showdown)
|
|
2807
2900
|
battle.rng.chance(u8, rate, 256)
|
|
2808
2901
|
else
|
|
@@ -2815,6 +2908,8 @@ pub const Rolls = struct {
|
|
|
2815
2908
|
fn unboost(battle: anytype, player: Player, options: anytype) !bool {
|
|
2816
2909
|
const proc = if (options.calc.overridden(player, .secondary_chance)) |val|
|
|
2817
2910
|
val == .true
|
|
2911
|
+
else if (@hasDecl(@TypeOf(battle.rng), "secondaryChance"))
|
|
2912
|
+
battle.rng.secondaryChance(player, 85)
|
|
2818
2913
|
else if (showdown)
|
|
2819
2914
|
battle.rng.chance(u8, 85, 256)
|
|
2820
2915
|
else
|
|
@@ -2827,6 +2922,8 @@ pub const Rolls = struct {
|
|
|
2827
2922
|
fn metronome(battle: anytype, player: Player, options: anytype) !Move {
|
|
2828
2923
|
const move: Move = if (options.calc.overridden(player, .metronome)) |val|
|
|
2829
2924
|
val
|
|
2925
|
+
else if (@hasDecl(@TypeOf(battle.rng), "metronome"))
|
|
2926
|
+
battle.rng.metronome(player)
|
|
2830
2927
|
else if (showdown)
|
|
2831
2928
|
Move.METRONOME[battle.rng.range(u8, 0, Move.METRONOME.len)]
|
|
2832
2929
|
else move: {
|
|
@@ -2845,6 +2942,8 @@ pub const Rolls = struct {
|
|
|
2845
2942
|
fn psywave(battle: anytype, player: Player, max: u8, options: anytype) !u8 {
|
|
2846
2943
|
const power = if (options.calc.overridden(player, .psywave)) |val|
|
|
2847
2944
|
val - 1
|
|
2945
|
+
else if (@hasDecl(@TypeOf(battle.rng), "psywave"))
|
|
2946
|
+
battle.rng.psywave(player)
|
|
2848
2947
|
else if (showdown)
|
|
2849
2948
|
battle.rng.range(u8, 0, max)
|
|
2850
2949
|
else power: {
|
|
@@ -2860,8 +2959,11 @@ pub const Rolls = struct {
|
|
|
2860
2959
|
}
|
|
2861
2960
|
|
|
2862
2961
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2863
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2864
|
-
@intCast(val)
|
|
2962
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2963
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2964
|
+
else
|
|
2965
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2966
|
+
battle.rng.sleepDuration(player)
|
|
2865
2967
|
else if (showdown)
|
|
2866
2968
|
battle.rng.range(u3, 1, 8)
|
|
2867
2969
|
else duration: {
|
|
@@ -2872,46 +2974,55 @@ pub const Rolls = struct {
|
|
|
2872
2974
|
};
|
|
2873
2975
|
|
|
2874
2976
|
assert(duration >= 1 and duration <= 7);
|
|
2875
|
-
options.chance.duration(
|
|
2977
|
+
options.chance.duration(player, duration);
|
|
2876
2978
|
return duration;
|
|
2877
2979
|
}
|
|
2878
2980
|
|
|
2879
2981
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2880
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2881
|
-
val
|
|
2982
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2983
|
+
if (val >= 1 and val <= 8) val else null
|
|
2984
|
+
else
|
|
2985
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2986
|
+
battle.rng.disableDuration(player)
|
|
2882
2987
|
else if (showdown)
|
|
2883
2988
|
battle.rng.range(u4, 1, 9)
|
|
2884
2989
|
else
|
|
2885
2990
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2886
2991
|
|
|
2887
2992
|
assert(duration >= 1 and duration <= 8);
|
|
2888
|
-
options.chance.duration(
|
|
2993
|
+
options.chance.duration(player, duration);
|
|
2889
2994
|
return duration;
|
|
2890
2995
|
}
|
|
2891
2996
|
|
|
2892
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2893
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2894
|
-
@intCast(val)
|
|
2997
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2998
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2999
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3000
|
+
else
|
|
3001
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3002
|
+
battle.rng.confusionDuration(player)
|
|
2895
3003
|
else if (showdown)
|
|
2896
3004
|
battle.rng.range(u3, 2, 6)
|
|
2897
3005
|
else
|
|
2898
3006
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2899
3007
|
|
|
2900
3008
|
assert(duration >= 2 and duration <= 5);
|
|
2901
|
-
options.chance.duration(
|
|
3009
|
+
options.chance.duration(player, duration);
|
|
2902
3010
|
return duration;
|
|
2903
3011
|
}
|
|
2904
3012
|
|
|
2905
3013
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2906
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2907
|
-
@intCast(val)
|
|
3014
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3015
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3016
|
+
else
|
|
3017
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
3018
|
+
battle.rng.attackingDuration(player)
|
|
2908
3019
|
else if (showdown)
|
|
2909
3020
|
battle.rng.range(u3, 2, 4)
|
|
2910
3021
|
else
|
|
2911
3022
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2912
3023
|
|
|
2913
3024
|
assert(duration >= 2 and duration <= 3);
|
|
2914
|
-
options.chance.duration(
|
|
3025
|
+
options.chance.duration(player, duration);
|
|
2915
3026
|
return duration;
|
|
2916
3027
|
}
|
|
2917
3028
|
|
|
@@ -2924,8 +3035,11 @@ pub const Rolls = struct {
|
|
|
2924
3035
|
options: anytype,
|
|
2925
3036
|
) !u3 {
|
|
2926
3037
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2927
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2928
|
-
@intCast(val)
|
|
3038
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3039
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3040
|
+
else
|
|
3041
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
3042
|
+
battle.rng.distribution(player)
|
|
2929
3043
|
else if (showdown)
|
|
2930
3044
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
2931
3045
|
else n: {
|
|
@@ -2935,7 +3049,7 @@ pub const Rolls = struct {
|
|
|
2935
3049
|
|
|
2936
3050
|
assert(n >= 2 and n <= 5);
|
|
2937
3051
|
if (effect == .Binding) {
|
|
2938
|
-
options.chance.duration(
|
|
3052
|
+
options.chance.duration(player, n);
|
|
2939
3053
|
} else {
|
|
2940
3054
|
try options.chance.multiHit(player, n);
|
|
2941
3055
|
}
|
|
@@ -2950,15 +3064,15 @@ pub const Rolls = struct {
|
|
|
2950
3064
|
options: anytype,
|
|
2951
3065
|
) !u4 {
|
|
2952
3066
|
// TODO: consider throwing error instead of rerolling?
|
|
2953
|
-
const
|
|
3067
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2954
3068
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2955
3069
|
val
|
|
2956
3070
|
else
|
|
2957
3071
|
null
|
|
2958
3072
|
else
|
|
2959
|
-
null
|
|
2960
|
-
|
|
2961
|
-
|
|
3073
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3074
|
+
battle.rng.moveSlot(player, moves, check_pp)
|
|
3075
|
+
else slot: {
|
|
2962
3076
|
if (showdown) {
|
|
2963
3077
|
if (check_pp == 0) {
|
|
2964
3078
|
var i: usize = moves.len;
|
|
@@ -3038,11 +3152,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3038
3152
|
const side = battle.side(player);
|
|
3039
3153
|
const foe = battle.foe(player);
|
|
3040
3154
|
|
|
3041
|
-
|
|
3155
|
+
const active = &side.active;
|
|
3042
3156
|
const stored = side.stored();
|
|
3043
3157
|
|
|
3044
3158
|
// While players are not given any input options on the cartridge in these cases,
|
|
3045
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3159
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3046
3160
|
//
|
|
3047
3161
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3048
3162
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3050,7 +3164,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3050
3164
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3051
3165
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3052
3166
|
// strictly correct as the player would not have been presented the option to move or
|
|
3053
|
-
// switch at all
|
|
3167
|
+
// switch at all
|
|
3054
3168
|
if (isForced(active)) {
|
|
3055
3169
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3056
3170
|
n += 1;
|
|
@@ -3069,7 +3183,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3069
3183
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3070
3184
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3071
3185
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3072
|
-
// move normally if sleeping/frozen/bound
|
|
3186
|
+
// move normally if sleeping/frozen/bound
|
|
3073
3187
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3074
3188
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3075
3189
|
{
|
|
@@ -3082,7 +3196,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3082
3196
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3083
3197
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3084
3198
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3085
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3199
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3086
3200
|
if (limited) {
|
|
3087
3201
|
assert(showdown);
|
|
3088
3202
|
assert(side.last_selected_move != .None);
|
|
@@ -3114,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3114
3228
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3115
3229
|
if (n == before) {
|
|
3116
3230
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3117
|
-
if (!
|
|
3231
|
+
if (!pkmn.options.mod) {
|
|
3118
3232
|
while (slot <= 4) : (slot += 1) {
|
|
3119
3233
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3120
3234
|
}
|