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