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