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