@pkmn/engine 0.1.0-dev.5570b5c5 → 0.1.0-dev.587596a6
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 -1
- package/README.md +29 -17
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +47 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +110 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +26 -4
- package/build/pkg/addon.js +34 -65
- package/build/pkg/addon.js.map +1 -1
- package/build/pkg/data/layout.json +15 -2
- package/build/pkg/data/protocol.json +23 -9
- package/build/pkg/data.d.ts +23 -1
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.d.ts +15 -18
- package/build/pkg/gen1.js +28 -19
- package/build/pkg/gen1.js.map +1 -1
- package/build/pkg/index.d.ts +40 -90
- package/build/pkg/index.js +23 -70
- package/build/pkg/index.js.map +1 -1
- package/build/pkg/protocol.d.ts +20 -1
- package/build/pkg/protocol.js +105 -103
- package/build/pkg/protocol.js.map +1 -1
- package/build/tools/debug.js +68 -74
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +152 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +152 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +359 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +33 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +237 -98
- package/package.json +59 -138
- package/src/bin/install-pkmn-engine +25 -7
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +200 -0
- package/src/lib/bindings/node.zig +117 -0
- package/src/lib/bindings/wasm.zig +50 -0
- package/src/lib/common/array.zig +70 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +32 -11
- package/src/lib/common/js.zig +3 -7
- package/src/lib/common/optional.zig +11 -17
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +148 -134
- package/src/lib/common/rational.zig +338 -255
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +33 -1
- package/src/lib/gen1/calc.zig +701 -171
- package/src/lib/gen1/chance.zig +407 -298
- package/src/lib/gen1/data/moves.zig +2 -4
- package/src/lib/gen1/data/species.zig +1 -3
- package/src/lib/gen1/data/types.zig +3 -4
- package/src/lib/gen1/data.zig +43 -46
- package/src/lib/gen1/helpers.zig +91 -549
- package/src/lib/gen1/mechanics.zig +355 -258
- package/src/lib/gen2/calc.zig +35 -28
- package/src/lib/gen2/chance.zig +91 -510
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +7 -5
- package/src/lib/gen2/data.zig +33 -35
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +45 -26
- package/src/lib/gen2/mechanics.zig +97 -124
- package/src/lib/napi.h +2 -0
- package/src/lib/node.zig +4 -121
- package/src/lib/pkmn.zig +106 -33
- package/src/lib/wasm.zig +9 -9
- package/src/tools/display/dom.ts +93 -0
- package/src/tools/display/ui.css +354 -0
- package/build/test/display.js +0 -400
- package/build/test/display.js.map +0 -1
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -1,37 +1,29 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const pkmn = @import("../pkmn.zig");
|
|
1
|
+
const DEBUG = @import("../common/debug.zig").print;
|
|
4
2
|
|
|
3
|
+
const builtin = @import("builtin");
|
|
4
|
+
const chance = @import("chance.zig");
|
|
5
5
|
const common = @import("../common/data.zig");
|
|
6
|
-
const
|
|
6
|
+
const data = @import("data.zig");
|
|
7
|
+
const pkmn = @import("../pkmn.zig");
|
|
7
8
|
const protocol = @import("../common/protocol.zig");
|
|
8
9
|
const rng = @import("../common/rng.zig");
|
|
9
|
-
|
|
10
|
-
const data = @import("data.zig");
|
|
10
|
+
const std = @import("std");
|
|
11
11
|
|
|
12
12
|
const assert = std.debug.assert;
|
|
13
|
-
|
|
14
|
-
const expectEqual = std.testing.expectEqual;
|
|
15
|
-
|
|
16
|
-
const showdown = pkmn.options.showdown;
|
|
17
|
-
|
|
18
|
-
const Choice = common.Choice;
|
|
19
|
-
const ID = common.ID;
|
|
20
|
-
const Player = common.Player;
|
|
21
|
-
const Result = common.Result;
|
|
22
|
-
|
|
23
13
|
const Boost = protocol.Boost;
|
|
14
|
+
const Choice = common.Choice;
|
|
24
15
|
const Damage = protocol.Damage;
|
|
16
|
+
const Effectiveness = data.Effectiveness;
|
|
17
|
+
const expectEqual = std.testing.expectEqual;
|
|
25
18
|
const Fail = protocol.Fail;
|
|
26
|
-
const Heal = protocol.Heal;
|
|
27
|
-
|
|
28
19
|
const Gen12 = rng.Gen12;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const Effectiveness = data.Effectiveness;
|
|
20
|
+
const Heal = protocol.Heal;
|
|
21
|
+
const ID = common.ID;
|
|
32
22
|
const Move = data.Move;
|
|
33
23
|
const MoveSlot = data.MoveSlot;
|
|
34
|
-
const
|
|
24
|
+
const Player = common.Player;
|
|
25
|
+
const Result = common.Result;
|
|
26
|
+
const showdown = pkmn.options.showdown;
|
|
35
27
|
const Side = data.Side;
|
|
36
28
|
const Species = data.Species;
|
|
37
29
|
const Stats = data.Stats;
|
|
@@ -166,7 +158,7 @@ fn selectMove(
|
|
|
166
158
|
// to satisfy the conditions of the Desync Clause Mod. However, because Binding is still
|
|
167
159
|
// set the selected move will not actually be used, it will just be reported as having
|
|
168
160
|
// been used (this differs from how Pokémon Showdown works, but its impossible to
|
|
169
|
-
// replicate the incorrect behavior with the correct mechanisms)
|
|
161
|
+
// replicate the incorrect behavior with the correct mechanisms)
|
|
170
162
|
saveMove(battle, player, choice);
|
|
171
163
|
} else {
|
|
172
164
|
assert(choice.data == 0);
|
|
@@ -174,7 +166,10 @@ fn selectMove(
|
|
|
174
166
|
if (foe_choice.type == .Switch) {
|
|
175
167
|
const last = side.active.move(battle.lastMove(player).index);
|
|
176
168
|
if (last.id == .Metronome) side.last_selected_move = last.id;
|
|
177
|
-
if (last.id == .MirrorMove)
|
|
169
|
+
if (last.id == .MirrorMove) {
|
|
170
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
171
|
+
side.last_selected_move = last.id;
|
|
172
|
+
}
|
|
178
173
|
}
|
|
179
174
|
}
|
|
180
175
|
return null;
|
|
@@ -253,11 +248,11 @@ fn switchIn(battle: anytype, player: Player, slot: u8, initial: bool, options: a
|
|
|
253
248
|
active.moves = incoming.moves;
|
|
254
249
|
|
|
255
250
|
statusModify(incoming.status, &active.stats);
|
|
251
|
+
options.chance.switched(player, slot);
|
|
256
252
|
|
|
257
253
|
foe.active.volatiles.Binding = false;
|
|
258
254
|
|
|
259
255
|
try options.log.switched(.{ battle.active(player), incoming });
|
|
260
|
-
options.chance.switched(player, side.order[0], out);
|
|
261
256
|
|
|
262
257
|
if (showdown and incoming.status == Status.TOX) {
|
|
263
258
|
incoming.status = Status.init(.PSN);
|
|
@@ -347,13 +342,13 @@ fn doTurn(
|
|
|
347
342
|
)) |r| return r;
|
|
348
343
|
if (!replace) {
|
|
349
344
|
if (player_choice.type != .Switch) {
|
|
350
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
345
|
+
if (try checkFaint(battle, foe_player, true, options)) |r| return r;
|
|
351
346
|
}
|
|
352
347
|
if (residual) try handleResidual(battle, player, options);
|
|
353
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
348
|
+
if (try checkFaint(battle, player, false, options)) |r| return r;
|
|
354
349
|
} else if (foe_choice.type == .Pass) return null;
|
|
355
350
|
|
|
356
|
-
options.chance.clearPending();
|
|
351
|
+
if (!showdown) options.chance.clearPending();
|
|
357
352
|
|
|
358
353
|
residual = true;
|
|
359
354
|
replace = battle.side(foe_player).stored().hp == 0;
|
|
@@ -369,10 +364,10 @@ fn doTurn(
|
|
|
369
364
|
)) |r| return r;
|
|
370
365
|
if (!replace) {
|
|
371
366
|
if (!calc and foe_choice.type != .Switch) {
|
|
372
|
-
if (try checkFaint(battle, player, options)) |r| return r;
|
|
367
|
+
if (try checkFaint(battle, player, true, options)) |r| return r;
|
|
373
368
|
}
|
|
374
369
|
if (residual) try handleResidual(battle, foe_player, options);
|
|
375
|
-
if (try checkFaint(battle, foe_player, options)) |r| return r;
|
|
370
|
+
if (try checkFaint(battle, foe_player, false, options)) |r| return r;
|
|
376
371
|
}
|
|
377
372
|
|
|
378
373
|
// Flinch is bugged on Pokémon Showdown because it gets implemented with a duration which causes
|
|
@@ -425,13 +420,13 @@ fn executeMove(
|
|
|
425
420
|
mslot = @intCast(battle.lastMove(player).index);
|
|
426
421
|
const stored = side.stored();
|
|
427
422
|
// GLITCH: Struggle bypass PP underflow via Hyper Beam / Trapping-switch auto selection
|
|
428
|
-
auto = isForced(&side.active) or
|
|
423
|
+
auto = pkmn.options.mod or isForced(&side.active) or
|
|
429
424
|
side.active.volatiles.Binding or side.active.volatiles.Bide or
|
|
430
425
|
side.last_selected_move == .HyperBeam or
|
|
431
426
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP);
|
|
432
427
|
// If it wasn't Hyper Beam or the continuation of a move effect then we must have just
|
|
433
428
|
// thawed, in which case we will desync unless the last_selected_move happened to be at
|
|
434
|
-
// index 1 and the current Pokémon has the same move in its first slot
|
|
429
|
+
// index 1 and the current Pokémon has the same move in its first slot
|
|
435
430
|
if (!auto) {
|
|
436
431
|
// side.active.moves(slot) is safe to check even though the slot in question might not
|
|
437
432
|
// technically be from this Pokémon because it must be exactly 1 to not desync and
|
|
@@ -458,6 +453,25 @@ fn executeMove(
|
|
|
458
453
|
.err => return @as(?Result, Result.Error),
|
|
459
454
|
}
|
|
460
455
|
|
|
456
|
+
// Pokémon Showdown incorrectly implements PP deduction when handling the Hyper Beam automatic
|
|
457
|
+
// selection glitch - if the move was proced due to Metronome / Mirror Move it either subtracts
|
|
458
|
+
// from the Hyper Beam slot if present or skips PP deduction entirely...
|
|
459
|
+
if (showdown and !skip_pp and side.last_selected_move == .HyperBeam and
|
|
460
|
+
side.last_selected_move != side.active.move(mslot).id)
|
|
461
|
+
{
|
|
462
|
+
assert(mslot == 1);
|
|
463
|
+
const has_beam = has_beam: {
|
|
464
|
+
for (side.active.moves, 0..) |m, i| {
|
|
465
|
+
if (m.id == .HyperBeam) {
|
|
466
|
+
mslot = @intCast(i + 1);
|
|
467
|
+
break :has_beam true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
break :has_beam false;
|
|
471
|
+
};
|
|
472
|
+
if (!has_beam) skip_pp = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
461
475
|
const can = skip_can or
|
|
462
476
|
try canMove(battle, player, mslot, auto, skip_pp, .None, residual, options);
|
|
463
477
|
if (!can) return null;
|
|
@@ -484,17 +498,22 @@ fn beforeMove(
|
|
|
484
498
|
|
|
485
499
|
if (Status.is(stored.status, .SLP)) {
|
|
486
500
|
const before = stored.status;
|
|
501
|
+
const slf = Status.is(stored.status, .EXT);
|
|
487
502
|
// Even if the EXT bit is set this will still correctly modify the sleep duration
|
|
488
|
-
if (options.calc.
|
|
489
|
-
|
|
503
|
+
if (options.calc.overridden(player, .sleep)) |obs| switch (obs) {
|
|
504
|
+
.started, .ended => stored.status = 0,
|
|
505
|
+
.continuing => if (Status.duration(stored.status) > 1) {
|
|
506
|
+
stored.status -= 1;
|
|
507
|
+
},
|
|
508
|
+
else => unreachable,
|
|
490
509
|
} else {
|
|
491
510
|
stored.status -= 1;
|
|
492
511
|
}
|
|
493
512
|
|
|
494
513
|
const duration = Status.duration(stored.status);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
514
|
+
try options.chance.sleep(player, if (slf)
|
|
515
|
+
.None
|
|
516
|
+
else if (duration == 0) .ended else .continuing);
|
|
498
517
|
|
|
499
518
|
if (duration == 0) {
|
|
500
519
|
try log.curestatus(.{ ident, before, .Message });
|
|
@@ -532,12 +551,12 @@ fn beforeMove(
|
|
|
532
551
|
}
|
|
533
552
|
|
|
534
553
|
if (volatiles.disable_duration > 0) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
554
|
+
volatiles.disable_duration =
|
|
555
|
+
decrement(.disable, player, options, volatiles.disable_duration);
|
|
556
|
+
try options.chance.disable(
|
|
557
|
+
player,
|
|
558
|
+
if (volatiles.disable_duration == 0) .ended else .continuing,
|
|
559
|
+
);
|
|
541
560
|
|
|
542
561
|
if (volatiles.disable_duration == 0) {
|
|
543
562
|
volatiles.disable_move = 0;
|
|
@@ -550,19 +569,24 @@ fn beforeMove(
|
|
|
550
569
|
// This can only happen if a Pokémon started the battle frozen/sleeping and was thawed/woken
|
|
551
570
|
// before the side had a selected a move - we simply need to assume this leads to a desync
|
|
552
571
|
if (side.last_selected_move == .None) {
|
|
553
|
-
assert(!
|
|
572
|
+
assert(!pkmn.options.mod);
|
|
554
573
|
return .err;
|
|
555
574
|
}
|
|
556
575
|
|
|
557
576
|
if (volatiles.Confusion) {
|
|
558
577
|
assert(volatiles.confusion > 0);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (
|
|
578
|
+
if (options.calc.overridden(player, .confusion)) |obs| switch (obs) {
|
|
579
|
+
.started, .ended => volatiles.confusion = 0,
|
|
580
|
+
.continuing => if (volatiles.confusion > 1) {
|
|
581
|
+
volatiles.confusion -= 1;
|
|
582
|
+
},
|
|
583
|
+
.overwritten => {},
|
|
584
|
+
else => unreachable,
|
|
562
585
|
} else {
|
|
563
586
|
volatiles.confusion -= 1;
|
|
564
587
|
}
|
|
565
|
-
|
|
588
|
+
|
|
589
|
+
try options.chance.confusion(player, if (volatiles.confusion == 0) .ended else .continuing);
|
|
566
590
|
|
|
567
591
|
if (volatiles.confusion == 0) {
|
|
568
592
|
volatiles.Confusion = false;
|
|
@@ -580,7 +604,9 @@ fn beforeMove(
|
|
|
580
604
|
volatiles.Charging = false;
|
|
581
605
|
volatiles.Binding = false;
|
|
582
606
|
volatiles.Invulnerable = false;
|
|
583
|
-
|
|
607
|
+
|
|
608
|
+
options.chance.observe(.attacking, player, .None);
|
|
609
|
+
options.chance.observe(.binding, player, .None);
|
|
584
610
|
{
|
|
585
611
|
// This feels (and is) disgusting but the cartridge literally just overwrites
|
|
586
612
|
// the opponent's defense with the user's defense and resets it after. As a
|
|
@@ -588,19 +614,14 @@ fn beforeMove(
|
|
|
588
614
|
const def = foe.active.stats.def;
|
|
589
615
|
foe.active.stats.def = active.stats.def;
|
|
590
616
|
defer foe.active.stats.def = def;
|
|
591
|
-
if (!calcDamage(battle, player, player.foe(), null, false))
|
|
617
|
+
if (!calcDamage(battle, player, player.foe(), null, false, options)) {
|
|
618
|
+
return .err;
|
|
619
|
+
}
|
|
592
620
|
}
|
|
593
|
-
// Pokémon Showdown incorrectly changes the "target" of the confusion self-hit based
|
|
594
|
-
// on the targeting behavior of the confused Pokémon's selected move which results
|
|
595
|
-
// in the wrong behavior with respect to the Substitute + Confusion glitch
|
|
596
|
-
const target = if (showdown and Move.get(side.last_selected_move).target == .Self)
|
|
597
|
-
player
|
|
598
|
-
else
|
|
599
|
-
player.foe();
|
|
600
621
|
|
|
601
622
|
const uncapped = battle.last_damage;
|
|
602
623
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
603
|
-
_ = try applyDamage(battle, player,
|
|
624
|
+
_ = try applyDamage(battle, player, player.foe(), .Confusion, options);
|
|
604
625
|
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
605
626
|
if (showdown) battle.last_damage = uncapped;
|
|
606
627
|
|
|
@@ -617,7 +638,10 @@ fn beforeMove(
|
|
|
617
638
|
volatiles.Thrashing = false;
|
|
618
639
|
volatiles.Charging = false;
|
|
619
640
|
volatiles.Binding = false;
|
|
620
|
-
|
|
641
|
+
|
|
642
|
+
options.chance.observe(.attacking, player, .None);
|
|
643
|
+
options.chance.observe(.binding, player, .None);
|
|
644
|
+
|
|
621
645
|
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
622
646
|
try log.cant(.{ ident, .Paralysis });
|
|
623
647
|
return .done;
|
|
@@ -635,12 +659,8 @@ fn beforeMove(
|
|
|
635
659
|
volatiles.state +%= battle.last_damage;
|
|
636
660
|
}
|
|
637
661
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
volatiles.attacks -= 1;
|
|
642
|
-
}
|
|
643
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
662
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
663
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
644
664
|
|
|
645
665
|
if (volatiles.attacks != 0) {
|
|
646
666
|
try log.activate(.{ ident, .Bide });
|
|
@@ -674,35 +694,40 @@ fn beforeMove(
|
|
|
674
694
|
}
|
|
675
695
|
|
|
676
696
|
if (volatiles.Thrashing) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
697
|
+
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
698
|
+
|
|
699
|
+
volatiles.attacks = decrement(.attacking, player, options, volatiles.attacks);
|
|
700
|
+
try options.chance.attacking(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
701
|
+
|
|
702
|
+
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
705
|
+
volatiles.Thrashing = false;
|
|
706
|
+
volatiles.Confusion = true;
|
|
707
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
683
708
|
|
|
684
|
-
|
|
709
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
710
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
711
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
712
|
+
// correct player)
|
|
685
713
|
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
714
|
+
options.chance.observe(.confusion, player, if (overwritten)
|
|
715
|
+
if (pkmn.options.overwrite) .overwritten else .continuing
|
|
716
|
+
else
|
|
717
|
+
.started);
|
|
718
|
+
if (!overwritten) options.calc.confusion(player);
|
|
686
719
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
if (handleThrashing(battle, active, player, options)) {
|
|
693
|
-
try log.start(.{ battle.active(player), .ConfusionSilent });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
720
|
+
|
|
721
|
+
// This shouldn't actually set last_used_move, but Pokémon Showdown sets last
|
|
722
|
+
// used in useMove and doesn't have the notion of skipping canMove semantics
|
|
723
|
+
if (showdown) side.last_used_move = side.last_selected_move;
|
|
724
|
+
|
|
696
725
|
return .skip_can;
|
|
697
726
|
}
|
|
698
727
|
|
|
699
728
|
if (volatiles.Binding) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
} else {
|
|
703
|
-
volatiles.attacks -= 1;
|
|
704
|
-
}
|
|
705
|
-
try options.chance.binding(player, volatiles.attacks);
|
|
729
|
+
volatiles.attacks = decrement(.binding, player, options, volatiles.attacks);
|
|
730
|
+
try options.chance.binding(player, if (volatiles.attacks == 0) .ended else .continuing);
|
|
706
731
|
|
|
707
732
|
try log.move(.{ ident, side.last_selected_move, battle.active(player.foe()) });
|
|
708
733
|
if (showdown or battle.last_damage != 0) {
|
|
@@ -756,9 +781,14 @@ fn canMove(
|
|
|
756
781
|
|
|
757
782
|
if (move.effect == .Thrashing) {
|
|
758
783
|
Effects.thrashing(battle, player, options);
|
|
759
|
-
} else if (
|
|
760
|
-
// Pokémon Showdown handles this after hit/miss checks and damage calculation
|
|
761
|
-
|
|
784
|
+
} else if (move.effect == .Binding) {
|
|
785
|
+
// Pokémon Showdown handles this after hit/miss checks and damage calculation, though clears
|
|
786
|
+
// Recharging in onTryMove (required for the Hyper Beam automatic selection glitch)
|
|
787
|
+
if (showdown) {
|
|
788
|
+
battle.foe(player).active.volatiles.Recharging = false;
|
|
789
|
+
} else {
|
|
790
|
+
try Effects.binding(battle, player, false, options);
|
|
791
|
+
}
|
|
762
792
|
}
|
|
763
793
|
|
|
764
794
|
return true;
|
|
@@ -795,7 +825,7 @@ fn decrementPP(side: *Side, mslot: u4, auto: bool) void {
|
|
|
795
825
|
assert(active.move(mslot).pp == side.stored().move(mslot).pp);
|
|
796
826
|
}
|
|
797
827
|
|
|
798
|
-
fn incrementPP(side: *Side, mslot: u4) void {
|
|
828
|
+
fn incrementPP(side: *Side, player: Player, mslot: u4, options: anytype) void {
|
|
799
829
|
var active = &side.active;
|
|
800
830
|
const volatiles = &active.volatiles;
|
|
801
831
|
|
|
@@ -803,8 +833,11 @@ fn incrementPP(side: *Side, mslot: u4) void {
|
|
|
803
833
|
// GLITCH: No check for Transform means an empty/incorrect stored slot can get incremented
|
|
804
834
|
if (showdown and volatiles.Transform) return;
|
|
805
835
|
|
|
836
|
+
const n = options.calc.overridden(player, .pp) orelse 1;
|
|
837
|
+
if (volatiles.Transform and !options.chance.pp(player, n)) return;
|
|
838
|
+
|
|
806
839
|
assert(mslot > 0 and mslot <= 4);
|
|
807
|
-
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +%
|
|
840
|
+
side.stored().moves[mslot - 1].pp = @as(u6, @intCast(side.stored().moves[mslot - 1].pp)) +% n;
|
|
808
841
|
}
|
|
809
842
|
|
|
810
843
|
// Pokémon Showdown does hit/multi/crit/damage instead of crit/damage/hit/multi
|
|
@@ -845,20 +878,20 @@ fn doMove(
|
|
|
845
878
|
var miss = showdown and miss: {
|
|
846
879
|
immune = move.target != .Self and !status and !counter and
|
|
847
880
|
(@intFromEnum(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
848
|
-
|
|
881
|
+
@intFromEnum(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
849
882
|
if (immune and move.effect != .Binding) break :miss true;
|
|
850
883
|
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) {
|
|
851
884
|
battle.last_damage = 0;
|
|
852
885
|
break :miss true;
|
|
853
886
|
}
|
|
854
887
|
break :miss (move.target != .Self and
|
|
855
|
-
!moveHit(battle, player, move, &immune, &mist, options));
|
|
888
|
+
!try moveHit(battle, player, move, &immune, &mist, options));
|
|
856
889
|
};
|
|
857
890
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
858
891
|
|
|
859
892
|
var late = showdown and move.effect != .Explode;
|
|
860
893
|
const skip = status or immune;
|
|
861
|
-
if ((!showdown or (!skip or counter)) and !miss)
|
|
894
|
+
if ((!showdown or (!skip or counter)) and !miss) damage: {
|
|
862
895
|
if (showdown and move.effect.isMulti()) {
|
|
863
896
|
try Effects.multiHit(battle, player, move, options);
|
|
864
897
|
hits = side.active.volatiles.attacks;
|
|
@@ -880,8 +913,9 @@ fn doMove(
|
|
|
880
913
|
ohko = if (!showdown) side.active.stats.spe >= foe.active.stats.spe else true;
|
|
881
914
|
// This can overflow after adjustDamage, but will still be sufficient to OHKO
|
|
882
915
|
battle.last_damage = if (ohko) 65535 else 0;
|
|
883
|
-
if (showdown) break :
|
|
884
|
-
} else if (!calcDamage(battle, player, player.foe(), move, crit)) {
|
|
916
|
+
if (showdown) break :damage; // skip adjustDamage / randomizeDamage
|
|
917
|
+
} else if (!calcDamage(battle, player, player.foe(), move, crit, options)) {
|
|
918
|
+
if (!showdown) try options.chance.commit(player, .err);
|
|
885
919
|
return @as(?Result, Result.Error);
|
|
886
920
|
}
|
|
887
921
|
if (battle.last_damage == 0) {
|
|
@@ -895,11 +929,12 @@ fn doMove(
|
|
|
895
929
|
}
|
|
896
930
|
}
|
|
897
931
|
|
|
932
|
+
var missed: bool = undefined;
|
|
898
933
|
const zero = battle.last_damage == 0;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
934
|
+
if (!showdown and !skip) {
|
|
935
|
+
missed = !try moveHit(battle, player, move, &immune, &mist, options);
|
|
936
|
+
miss = missed or zero;
|
|
937
|
+
}
|
|
903
938
|
|
|
904
939
|
assert(showdown or miss or battle.last_damage > 0 or skip);
|
|
905
940
|
assert((!showdown and miss) or !(ohko and immune));
|
|
@@ -926,11 +961,32 @@ fn doMove(
|
|
|
926
961
|
try log.immune(.{ foe_ident, .OHKO });
|
|
927
962
|
} else if (immune and !invulnerable and (!showdown or move.effect != .Binding)) {
|
|
928
963
|
try log.immune(.{ foe_ident, .None });
|
|
964
|
+
if (!showdown) {
|
|
965
|
+
if (move.effect == .Binding) {
|
|
966
|
+
// Rather obnoxiously, the cartridge sets the Binding volatile in canMove and
|
|
967
|
+
// then simply clears it later on if the move happens to miss in moveHit.
|
|
968
|
+
// However, moveHit can return true and we'll still end up in the outer miss
|
|
969
|
+
// handling block because attacking into an immune Pokemon does zero damage
|
|
970
|
+
// (considered missing)
|
|
971
|
+
assert(zero);
|
|
972
|
+
// Thus we still need to actually save the hit result (but not the crit roll).
|
|
973
|
+
// Using .miss for this commit would accomplish what we want, but is somewhat
|
|
974
|
+
// misleading as its entirely possible the move actually did hit (and the target
|
|
975
|
+
// is now trapped) so we use the .binding value instead which is effectively
|
|
976
|
+
// identical, just doesn't claim to know about the actual results of the hit
|
|
977
|
+
// roll)
|
|
978
|
+
try options.chance.commit(player, .binding);
|
|
979
|
+
} else {
|
|
980
|
+
// The .glitch option allows for conditionally committing if we are in a
|
|
981
|
+
// division-by-zero freeze alternative timeline scenario
|
|
982
|
+
try options.chance.commit(player, .glitch);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
929
985
|
} else if (mist) {
|
|
930
986
|
if (!foe.active.volatiles.Substitute) try log.activate(.{ foe_ident, .Mist });
|
|
931
987
|
try log.fail(.{ foe_ident, .None });
|
|
932
988
|
} else {
|
|
933
|
-
if (showdown
|
|
989
|
+
if (!showdown) try options.chance.commit(player, if (zero and !missed) .hit else .miss);
|
|
934
990
|
try log.lastmiss(.{});
|
|
935
991
|
try log.miss(.{battle.active(player)});
|
|
936
992
|
}
|
|
@@ -945,17 +1001,15 @@ fn doMove(
|
|
|
945
1001
|
try buildRage(battle, player.foe(), options);
|
|
946
1002
|
} else if (showdown and move.effect == .Disable) {
|
|
947
1003
|
try buildRage(battle, player.foe(), options);
|
|
948
|
-
} else if (!showdown and immune and move.effect == .Binding) {
|
|
949
|
-
try options.chance.commit(player, .binding);
|
|
950
1004
|
}
|
|
951
1005
|
return null;
|
|
952
|
-
} else {
|
|
1006
|
+
} else if (!showdown) {
|
|
953
1007
|
try options.chance.commit(player, .hit);
|
|
954
1008
|
}
|
|
955
1009
|
|
|
956
1010
|
// On the cartridge MultiHit doesn't get set up until after damage has been applied for the
|
|
957
1011
|
// first time but its more convenient and efficient to set it up here (Pokémon Showdown sets
|
|
958
|
-
// it up above before damage calculation)
|
|
1012
|
+
// it up above before damage calculation)
|
|
959
1013
|
if (!showdown and move.effect.isMulti()) {
|
|
960
1014
|
try Effects.multiHit(battle, player, move, options);
|
|
961
1015
|
hits = side.active.volatiles.attacks;
|
|
@@ -965,7 +1019,7 @@ fn doMove(
|
|
|
965
1019
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
966
1020
|
|
|
967
1021
|
var nullified = false;
|
|
968
|
-
var hit:
|
|
1022
|
+
var hit: usize = 0;
|
|
969
1023
|
while (hit < hits) {
|
|
970
1024
|
if (hit == 0) {
|
|
971
1025
|
if (crit) try log.crit(.{battle.active(player.foe())});
|
|
@@ -993,33 +1047,31 @@ fn doMove(
|
|
|
993
1047
|
if (showdown and move.effect == .Twineedle and !nullified and foe.stored().hp > 0) {
|
|
994
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1049
|
}
|
|
996
|
-
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
1050
|
+
try log.hitcount(.{ battle.active(player.foe()), @as(u4, @intCast(hit)) });
|
|
1051
|
+
} else if (showdown and move.effect == .Binding) {
|
|
998
1052
|
// This should be handled much earlier but Pokémon Showdown does it here... ¯\_(ツ)_/¯
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
try buildRage(battle, player.foe(), options);
|
|
1010
|
-
}
|
|
1011
|
-
return null;
|
|
1053
|
+
try Effects.binding(battle, player, rewrap, options);
|
|
1054
|
+
if (immune) {
|
|
1055
|
+
battle.last_damage = 0;
|
|
1056
|
+
assert(foe.stored().hp > 0);
|
|
1057
|
+
// Pokémon Showdown logs |-damage| here instead of |-immune| because logic...
|
|
1058
|
+
if (sub) {
|
|
1059
|
+
try log.activate(.{ battle.active(player.foe()), .Substitute });
|
|
1060
|
+
} else {
|
|
1061
|
+
try log.damage(.{ battle.active(player.foe()), foe.stored(), .None });
|
|
1062
|
+
try buildRage(battle, player.foe(), options);
|
|
1012
1063
|
}
|
|
1064
|
+
return null;
|
|
1013
1065
|
}
|
|
1014
1066
|
}
|
|
1015
1067
|
|
|
1016
1068
|
// Substitute being broken nullifies the move's effect completely so even
|
|
1017
|
-
// if an effect was intended to "always happen" it will still get skipped
|
|
1069
|
+
// if an effect was intended to "always happen" it will still get skipped
|
|
1018
1070
|
if (nullified) return null;
|
|
1019
1071
|
|
|
1020
1072
|
// On the cartridge, "always happen" effect handlers are called in the applyDamage loop above,
|
|
1021
1073
|
// but this is only done to setup the MultiHit looping in the first place. Moving the MultiHit
|
|
1022
|
-
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1074
|
+
// setup before the loop means we can avoid having to waste time doing no-op handler searches
|
|
1023
1075
|
if (move.effect.alwaysHappens()) try alwaysHappens(battle, player, move, residual, options);
|
|
1024
1076
|
|
|
1025
1077
|
if (foe.stored().hp == 0) return null;
|
|
@@ -1034,7 +1086,7 @@ fn doMove(
|
|
|
1034
1086
|
// below). For Twineedle we change the data to that of one with PoisonChance1 given its
|
|
1035
1087
|
// MultiHit behavior is complete after the loop above, though Pokémon Showdown handles the
|
|
1036
1088
|
// Twineedle secondary effect in the MultiHit cleanup block above because it incorrectly
|
|
1037
|
-
// puts the |-status| message before |-hitcount| instead of after
|
|
1089
|
+
// puts the |-status| message before |-hitcount| instead of after
|
|
1038
1090
|
if (move.effect == .Twineedle) {
|
|
1039
1091
|
if (!showdown) try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
1040
1092
|
} else if (move.effect == .Disable) {
|
|
@@ -1058,7 +1110,9 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data, options: a
|
|
|
1058
1110
|
rate = if (side.active.volatiles.FocusEnergy) rate / 2 else @min(rate * 2, 255);
|
|
1059
1111
|
rate = if (move.effect == .HighCritical) @min(rate * 4, 255) else rate / 2;
|
|
1060
1112
|
|
|
1061
|
-
|
|
1113
|
+
const spurious =
|
|
1114
|
+
!showdown and ((move.bp == 0 and move.effect != .OHKO) or move.target == .Depends);
|
|
1115
|
+
return Rolls.criticalHit(battle, player, @intCast(rate), spurious, options);
|
|
1062
1116
|
}
|
|
1063
1117
|
|
|
1064
1118
|
fn calcDamage(
|
|
@@ -1067,6 +1121,7 @@ fn calcDamage(
|
|
|
1067
1121
|
target_player: Player,
|
|
1068
1122
|
m: ?Move.Data,
|
|
1069
1123
|
crit: bool,
|
|
1124
|
+
options: anytype,
|
|
1070
1125
|
) bool {
|
|
1071
1126
|
// Confusion (indicated when m == null) just needs a 40 BP physical move
|
|
1072
1127
|
const cfz = m == null;
|
|
@@ -1078,28 +1133,8 @@ fn calcDamage(
|
|
|
1078
1133
|
|
|
1079
1134
|
const special = move.type.special();
|
|
1080
1135
|
|
|
1081
|
-
|
|
1082
|
-
var
|
|
1083
|
-
if (crit)
|
|
1084
|
-
if (special) side.stored().stats.spc
|
|
1085
|
-
else side.stored().stats.atk
|
|
1086
|
-
else
|
|
1087
|
-
if (special) side.active.stats.spc
|
|
1088
|
-
else side.active.stats.atk;
|
|
1089
|
-
var def: u32 =
|
|
1090
|
-
if (crit)
|
|
1091
|
-
if (special) target.stored().stats.spc
|
|
1092
|
-
else target.stored().stats.def
|
|
1093
|
-
else
|
|
1094
|
-
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1095
|
-
if (special)
|
|
1096
|
-
target.active.stats.spc *
|
|
1097
|
-
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1098
|
-
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1099
|
-
else
|
|
1100
|
-
target.active.stats.def *
|
|
1101
|
-
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1102
|
-
// zig fmt: on
|
|
1136
|
+
var atk = attack(side, crit, special);
|
|
1137
|
+
var def = defense(target, crit, special, cfz);
|
|
1103
1138
|
|
|
1104
1139
|
// Pokémon Showdown erroneously skips this for confusion's self-hit damage, but thankfully we
|
|
1105
1140
|
// will not overflow because the hit is only 40 BP and unboosted (the highest legal unboosted
|
|
@@ -1107,7 +1142,7 @@ fn calcDamage(
|
|
|
1107
1142
|
if ((!showdown or !cfz) and (atk > 255 or def > 255)) {
|
|
1108
1143
|
atk = @max((atk / 4) & 255, 1);
|
|
1109
1144
|
// GLITCH: not adjusted to be a min of 1 on cartridge (can lead to division-by-zero freeze)
|
|
1110
|
-
def = @max((def / 4) & 255, if (
|
|
1145
|
+
def = @max((def / 4) & 255, if (pkmn.options.mod) 1 else 0);
|
|
1111
1146
|
}
|
|
1112
1147
|
|
|
1113
1148
|
const lvl: u32 = side.stored().level * @as(u2, if (crit) 2 else 1);
|
|
@@ -1124,11 +1159,41 @@ fn calcDamage(
|
|
|
1124
1159
|
d = @min(997, d);
|
|
1125
1160
|
d += 2;
|
|
1126
1161
|
|
|
1162
|
+
// Whether or not a critical hit occurred can change whether a division-by-zero freeze is
|
|
1163
|
+
// encountered so we need to make sure that in the event of a miss we commit the status of the
|
|
1164
|
+
// crit roll. The alternative to doing this extra work is to *always* commit the crit roll but
|
|
1165
|
+
// that results in counter intuitive probabilities in the common case
|
|
1166
|
+
if (pkmn.options.chance and !pkmn.options.mod and move.effect != .Explode) {
|
|
1167
|
+
atk = attack(side, !crit, special);
|
|
1168
|
+
def = defense(target, !crit, special, false);
|
|
1169
|
+
if ((atk > 255 or def > 255) and (def / 4) & 255 == 0) options.chance.glitch();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1127
1172
|
battle.last_damage = @intCast(d);
|
|
1128
1173
|
|
|
1129
1174
|
return true;
|
|
1130
1175
|
}
|
|
1131
1176
|
|
|
1177
|
+
fn attack(side: *Side, crit: bool, special: bool) u32 {
|
|
1178
|
+
return if (crit)
|
|
1179
|
+
if (special) side.stored().stats.spc else side.stored().stats.atk
|
|
1180
|
+
else if (special) side.active.stats.spc else side.active.stats.atk;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fn defense(target: *Side, crit: bool, special: bool, cfz: bool) u32 {
|
|
1184
|
+
return if (crit)
|
|
1185
|
+
if (special) target.stored().stats.spc else target.stored().stats.def
|
|
1186
|
+
else
|
|
1187
|
+
// GLITCH: not capped to MAX_STAT_VALUE, can be 999 * 2 = 1998
|
|
1188
|
+
if (special)
|
|
1189
|
+
target.active.stats.spc *
|
|
1190
|
+
@as(u2, if (target.active.volatiles.LightScreen) 2 else 1)
|
|
1191
|
+
else
|
|
1192
|
+
// Pokémon Showdown doesn't apply the opponent's Reflect to confusion's self-hit
|
|
1193
|
+
target.active.stats.def *
|
|
1194
|
+
@as(u2, if ((!showdown or !cfz) and target.active.volatiles.Reflect) 2 else 1);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1132
1197
|
fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
1133
1198
|
const side = battle.side(player);
|
|
1134
1199
|
const foe = battle.foe(player);
|
|
@@ -1173,7 +1238,6 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1173
1238
|
const foe = battle.foe(player);
|
|
1174
1239
|
|
|
1175
1240
|
if (!try checkHit(battle, player, move, options)) return null;
|
|
1176
|
-
try options.chance.commit(player, .hit);
|
|
1177
1241
|
|
|
1178
1242
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1179
1243
|
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
@@ -1184,13 +1248,13 @@ fn specialDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1184
1248
|
.Psywave => power: {
|
|
1185
1249
|
const max: u8 = @intCast(@as(u16, side.stored().level) * 3 / 2);
|
|
1186
1250
|
// GLITCH: Psywave infinite glitch loop
|
|
1187
|
-
if (!
|
|
1251
|
+
if (!pkmn.options.mod and max <= 1) return Result.Error;
|
|
1188
1252
|
break :power try Rolls.psywave(battle, player, max, options);
|
|
1189
1253
|
},
|
|
1190
1254
|
else => unreachable,
|
|
1191
1255
|
};
|
|
1192
1256
|
|
|
1193
|
-
if (battle.last_damage == 0) return if (
|
|
1257
|
+
if (battle.last_damage == 0) return if (pkmn.options.mod) null else Result.Error;
|
|
1194
1258
|
|
|
1195
1259
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1196
1260
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1210,15 +1274,15 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1210
1274
|
// Pretend Splash was used as a stand-in when no move has been used to fail below with 0 BP
|
|
1211
1275
|
const foe_last_selected_move =
|
|
1212
1276
|
Move.get(if (foe.last_selected_move == .None or foe.last_selected_move == .SKIP_TURN)
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1277
|
+
.Splash
|
|
1278
|
+
else
|
|
1279
|
+
foe.last_selected_move);
|
|
1216
1280
|
|
|
1217
1281
|
const used = battle.lastMove(player.foe()).counterable != 0;
|
|
1218
1282
|
const selected = foe_last_selected_move.bp > 0 and
|
|
1219
1283
|
foe.last_selected_move != .Counter and
|
|
1220
1284
|
(foe_last_selected_move.type == .Normal or
|
|
1221
|
-
|
|
1285
|
+
foe_last_selected_move.type == .Fighting);
|
|
1222
1286
|
|
|
1223
1287
|
if (!used and !selected) {
|
|
1224
1288
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -1227,16 +1291,19 @@ fn counterDamage(battle: anytype, player: Player, move: Move.Data, options: anyt
|
|
|
1227
1291
|
|
|
1228
1292
|
if (!used or !selected) {
|
|
1229
1293
|
// GLITCH: Counter desync (covered by Desync Clause Mod on Pokémon Showdown)
|
|
1230
|
-
if (!
|
|
1294
|
+
if (!pkmn.options.mod) return Result.Error;
|
|
1231
1295
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1232
1296
|
return null;
|
|
1233
1297
|
}
|
|
1234
1298
|
|
|
1235
|
-
|
|
1299
|
+
// NOTE: llvm/llvm-project#58557
|
|
1300
|
+
battle.last_damage = if (comptime builtin.target.cpu.arch.isWasm())
|
|
1301
|
+
@max(battle.last_damage *% 2, std.math.maxInt(u16))
|
|
1302
|
+
else
|
|
1303
|
+
battle.last_damage *| 2;
|
|
1236
1304
|
|
|
1237
|
-
// Pokémon Showdown calls
|
|
1305
|
+
// Pokémon Showdown calls moveHit before Counter
|
|
1238
1306
|
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1239
|
-
try options.chance.commit(player, .hit);
|
|
1240
1307
|
|
|
1241
1308
|
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1242
1309
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, options);
|
|
@@ -1265,7 +1332,7 @@ fn applyDamage(
|
|
|
1265
1332
|
subbed.active.volatiles.Substitute = false;
|
|
1266
1333
|
// battle.last_damage is not updated with the amount of HP the Substitute had
|
|
1267
1334
|
try options.log.end(.{ battle.active(sub_player), .Substitute });
|
|
1268
|
-
options.calc.capped(
|
|
1335
|
+
if (reason != .Confusion) options.calc.capped(sub_player);
|
|
1269
1336
|
return true;
|
|
1270
1337
|
} else {
|
|
1271
1338
|
// Safe to truncate since less than subbed.volatiles.substitute which is a u8
|
|
@@ -1299,7 +1366,7 @@ fn mirrorMove(
|
|
|
1299
1366
|
try options.log.fail(.{ battle.active(player), .None });
|
|
1300
1367
|
return null;
|
|
1301
1368
|
} else if (!showdown or foe.last_used_move != .Struggle) {
|
|
1302
|
-
incrementPP(side, mslot);
|
|
1369
|
+
incrementPP(side, player, mslot, options);
|
|
1303
1370
|
}
|
|
1304
1371
|
|
|
1305
1372
|
if (!try canMove(battle, player, mslot, auto, false, .MirrorMove, residual, options)) {
|
|
@@ -1320,7 +1387,7 @@ fn metronome(
|
|
|
1320
1387
|
var side = battle.side(player);
|
|
1321
1388
|
|
|
1322
1389
|
side.last_selected_move = try Rolls.metronome(battle, player, options);
|
|
1323
|
-
incrementPP(side, mslot);
|
|
1390
|
+
incrementPP(side, player, mslot, options);
|
|
1324
1391
|
|
|
1325
1392
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, options)) {
|
|
1326
1393
|
return null;
|
|
@@ -1332,7 +1399,10 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1332
1399
|
var immune = false;
|
|
1333
1400
|
var mist = false;
|
|
1334
1401
|
|
|
1335
|
-
if (moveHit(battle, player, move, &immune, &mist, options))
|
|
1402
|
+
if (try moveHit(battle, player, move, &immune, &mist, options)) {
|
|
1403
|
+
if (!showdown) try options.chance.commit(player, .hit);
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1336
1406
|
|
|
1337
1407
|
assert(!immune);
|
|
1338
1408
|
if (mist) {
|
|
@@ -1341,11 +1411,11 @@ fn checkHit(battle: anytype, player: Player, move: Move.Data, options: anytype)
|
|
|
1341
1411
|
try options.log.activate(.{ foe_ident, .Mist });
|
|
1342
1412
|
try options.log.fail(.{ foe_ident, .None });
|
|
1343
1413
|
} else {
|
|
1344
|
-
try options.chance.commit(player, .miss);
|
|
1345
1414
|
try options.log.lastmiss(.{});
|
|
1346
1415
|
try options.log.miss(.{battle.active(player)});
|
|
1347
1416
|
}
|
|
1348
1417
|
|
|
1418
|
+
if (!showdown) try options.chance.commit(player, .miss);
|
|
1349
1419
|
return false;
|
|
1350
1420
|
}
|
|
1351
1421
|
|
|
@@ -1356,7 +1426,7 @@ fn moveHit(
|
|
|
1356
1426
|
immune: *bool,
|
|
1357
1427
|
mist: *bool,
|
|
1358
1428
|
options: anytype,
|
|
1359
|
-
) bool {
|
|
1429
|
+
) !bool {
|
|
1360
1430
|
var side = battle.side(player);
|
|
1361
1431
|
const foe = battle.foe(player);
|
|
1362
1432
|
|
|
@@ -1376,7 +1446,16 @@ fn moveHit(
|
|
|
1376
1446
|
break :miss true;
|
|
1377
1447
|
}
|
|
1378
1448
|
if (!showdown) {
|
|
1379
|
-
if (move.effect == .Swift)
|
|
1449
|
+
if (move.effect == .Swift) {
|
|
1450
|
+
// If Swift could cause a division-by-zero freeze (which doesn't exist on Pokémon
|
|
1451
|
+
// Showdown, hence why the block above doesn't include this code), we need the
|
|
1452
|
+
// damage roll to be committed. However, the commit code will not save this roll if
|
|
1453
|
+
// the glitch occurs and the move was not marked as having hit. To work around this
|
|
1454
|
+
// we flag that the move hit (as Swift trivially does), but pass the probability of
|
|
1455
|
+
// the hit as zero to ensure it doesn't actually get saved.
|
|
1456
|
+
try options.chance.hit(player, true, 0);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1380
1459
|
if (foe.active.volatiles.Invulnerable) break :miss true;
|
|
1381
1460
|
}
|
|
1382
1461
|
|
|
@@ -1407,7 +1486,7 @@ fn moveHit(
|
|
|
1407
1486
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1487
|
|
|
1409
1488
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1489
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1490
|
};
|
|
1412
1491
|
|
|
1413
1492
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1497,17 @@ fn moveHit(
|
|
|
1418
1497
|
|
|
1419
1498
|
if (!miss) return true;
|
|
1420
1499
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1500
|
+
|
|
1421
1501
|
side.active.volatiles.Binding = false;
|
|
1502
|
+
options.chance.observe(.binding, player, .None);
|
|
1503
|
+
|
|
1422
1504
|
return false;
|
|
1423
1505
|
}
|
|
1424
1506
|
|
|
1425
1507
|
fn checkFaint(
|
|
1426
1508
|
battle: anytype,
|
|
1427
1509
|
player: Player,
|
|
1510
|
+
cap: bool,
|
|
1428
1511
|
options: anytype,
|
|
1429
1512
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1513
|
const side = battle.side(player);
|
|
@@ -1437,8 +1520,8 @@ fn checkFaint(
|
|
|
1437
1520
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1521
|
const more = player_out or foe_out;
|
|
1439
1522
|
|
|
1440
|
-
if (try faint(battle, player, !(more or foe_fainted), options)) |r| return r;
|
|
1441
|
-
if (foe_fainted) if (try faint(battle, player.foe(), !more, options)) |r| return r;
|
|
1523
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1524
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1525
|
|
|
1443
1526
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1527
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1542,7 @@ fn checkFaint(
|
|
|
1459
1542
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1543
|
}
|
|
1461
1544
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1545
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1546
|
var side = battle.side(player);
|
|
1464
1547
|
var foe = battle.foe(player);
|
|
1465
1548
|
assert(side.stored().hp == 0);
|
|
@@ -1468,7 +1551,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1468
1551
|
assert(!foe_volatiles.MultiHit);
|
|
1469
1552
|
if (foe_volatiles.Bide) {
|
|
1470
1553
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1471
|
-
foe_volatiles.state = if (
|
|
1554
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1472
1555
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1473
1556
|
}
|
|
1474
1557
|
|
|
@@ -1493,7 +1576,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1576
|
}
|
|
1494
1577
|
|
|
1495
1578
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1579
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1580
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1581
|
+
if (cap) options.calc.capped(player);
|
|
1497
1582
|
return null;
|
|
1498
1583
|
}
|
|
1499
1584
|
|
|
@@ -1557,12 +1642,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1557
1642
|
|
|
1558
1643
|
battle.turn += 1;
|
|
1559
1644
|
|
|
1560
|
-
if (
|
|
1645
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1561
1646
|
try options.log.tie(.{});
|
|
1562
1647
|
return Result.Tie;
|
|
1563
1648
|
}
|
|
1564
1649
|
|
|
1565
|
-
if (
|
|
1650
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1566
1651
|
try options.log.tie(.{});
|
|
1567
1652
|
return Result.Tie;
|
|
1568
1653
|
} else if (battle.turn >= 65535) {
|
|
@@ -1653,17 +1738,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1738
|
}
|
|
1654
1739
|
}
|
|
1655
1740
|
|
|
1656
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon, player: Player, options: anytype) bool {
|
|
1657
|
-
var volatiles = &active.volatiles;
|
|
1658
|
-
assert(volatiles.Thrashing);
|
|
1659
|
-
if (volatiles.attacks > 0) return false;
|
|
1660
|
-
|
|
1661
|
-
volatiles.Thrashing = false;
|
|
1662
|
-
volatiles.Confusion = true;
|
|
1663
|
-
volatiles.confusion = Rolls.confusionDuration(battle, player, true, options);
|
|
1664
|
-
return true;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
1741
|
fn onBegin(
|
|
1668
1742
|
battle: anytype,
|
|
1669
1743
|
player: Player,
|
|
@@ -1750,8 +1824,10 @@ pub const Effects = struct {
|
|
|
1750
1824
|
|
|
1751
1825
|
side.active.volatiles.Bide = true;
|
|
1752
1826
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1827
|
+
|
|
1753
1828
|
side.active.volatiles.state = 0;
|
|
1754
1829
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1830
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1831
|
|
|
1756
1832
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1833
|
}
|
|
@@ -1813,12 +1889,14 @@ pub const Effects = struct {
|
|
|
1813
1889
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1890
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1891
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1892
|
}
|
|
1818
1893
|
|
|
1819
1894
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1895
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
1896
|
+
|
|
1897
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1898
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1899
|
+
options.calc.confusion(player.foe());
|
|
1822
1900
|
|
|
1823
1901
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1902
|
}
|
|
@@ -1846,10 +1924,7 @@ pub const Effects = struct {
|
|
|
1846
1924
|
const foe_ident = battle.active(player.foe());
|
|
1847
1925
|
|
|
1848
1926
|
// Pokémon Showdown handles hit/miss earlier in doMove
|
|
1849
|
-
if (!showdown)
|
|
1850
|
-
if (!try checkHit(battle, player, move, options)) return null;
|
|
1851
|
-
try options.chance.commit(player, .hit);
|
|
1852
|
-
}
|
|
1927
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1928
|
|
|
1854
1929
|
if (volatiles.disable_move != 0) {
|
|
1855
1930
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1867,18 +1942,18 @@ pub const Effects = struct {
|
|
|
1867
1942
|
|
|
1868
1943
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1869
1944
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1870
|
-
if (n == 0) {
|
|
1945
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1871
1946
|
try options.log.fail(.{ foe_ident, .None });
|
|
1872
1947
|
return null;
|
|
1873
1948
|
} else if (err) {
|
|
1874
1949
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1875
|
-
assert(!showdown);
|
|
1876
1950
|
return Result.Error;
|
|
1877
1951
|
}
|
|
1878
1952
|
|
|
1879
1953
|
volatiles.disable_move =
|
|
1880
1954
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1955
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1956
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1957
|
|
|
1883
1958
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1959
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1949,7 +2024,7 @@ pub const Effects = struct {
|
|
|
1949
2024
|
if (foe.active.types.includes(move.type)) return;
|
|
1950
2025
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1951
2026
|
// Freeze Clause Mod
|
|
1952
|
-
if (
|
|
2027
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1953
2028
|
|
|
1954
2029
|
foe_stored.status = Status.init(.FRZ);
|
|
1955
2030
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1985,22 +2060,24 @@ pub const Effects = struct {
|
|
|
1985
2060
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2061
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2062
|
s.stored().status = 0;
|
|
2063
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2064
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2065
|
s.stored().status = Status.init(.PSN);
|
|
1990
2066
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2067
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2068
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2069
|
}
|
|
1994
2070
|
} else {
|
|
1995
2071
|
if (Status.any(foe_stored.status)) {
|
|
1996
2072
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2073
|
foe.last_selected_move = .SKIP_TURN;
|
|
2074
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2075
|
}
|
|
1999
2076
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2077
|
foe_stored.status = 0;
|
|
2001
2078
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2079
|
+
try clearVolatiles(battle, player, options);
|
|
2080
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2081
|
}
|
|
2005
2082
|
}
|
|
2006
2083
|
|
|
@@ -2051,7 +2128,6 @@ pub const Effects = struct {
|
|
|
2051
2128
|
}
|
|
2052
2129
|
}
|
|
2053
2130
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2131
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2132
|
|
|
2057
2133
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2168,10 @@ pub const Effects = struct {
|
|
|
2092
2168
|
if (!has_mimic) {
|
|
2093
2169
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2170
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2171
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2172
|
}
|
|
2098
2173
|
}
|
|
2099
2174
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2175
|
|
|
2102
2176
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2177
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2225,6 @@ pub const Effects = struct {
|
|
|
2151
2225
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2226
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2227
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2228
|
|
|
2156
2229
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2230
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2292,6 @@ pub const Effects = struct {
|
|
|
2219
2292
|
|
|
2220
2293
|
if (move.effect == .Poison) {
|
|
2221
2294
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2295
|
} else {
|
|
2224
2296
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2297
|
return;
|
|
@@ -2288,13 +2360,12 @@ pub const Effects = struct {
|
|
|
2288
2360
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2361
|
});
|
|
2290
2362
|
}
|
|
2291
|
-
// If
|
|
2363
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2364
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2365
|
}
|
|
2295
2366
|
|
|
2296
2367
|
// Sleep Clause Mod
|
|
2297
|
-
if (
|
|
2368
|
+
if (pkmn.options.mod) {
|
|
2298
2369
|
for (foe.pokemon) |p| {
|
|
2299
2370
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2300
2371
|
}
|
|
@@ -2302,6 +2373,8 @@ pub const Effects = struct {
|
|
|
2302
2373
|
foe.active.volatiles.Recharging = false;
|
|
2303
2374
|
|
|
2304
2375
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2376
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2377
|
+
|
|
2305
2378
|
const last = battle.side(player).last_selected_move;
|
|
2306
2379
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2380
|
}
|
|
@@ -2343,9 +2416,7 @@ pub const Effects = struct {
|
|
|
2343
2416
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2417
|
if (showdown) {
|
|
2345
2418
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2419
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2420
|
battle.last_damage = 0;
|
|
2350
2421
|
} else {
|
|
2351
2422
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2431,8 @@ pub const Effects = struct {
|
|
|
2360
2431
|
|
|
2361
2432
|
volatiles.Thrashing = true;
|
|
2362
2433
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2434
|
+
|
|
2435
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2436
|
}
|
|
2364
2437
|
|
|
2365
2438
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2398,13 +2471,14 @@ pub const Effects = struct {
|
|
|
2398
2471
|
if (side.active.volatiles.Binding) return;
|
|
2399
2472
|
side.active.volatiles.Binding = true;
|
|
2400
2473
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2401
|
-
// (Pokémon Showdown
|
|
2474
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2402
2475
|
if (!showdown) {
|
|
2403
2476
|
foe.active.volatiles.Recharging = false;
|
|
2404
2477
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2405
2478
|
|
|
2406
2479
|
side.active.volatiles.attacks =
|
|
2407
2480
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2481
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2482
|
}
|
|
2409
2483
|
|
|
2410
2484
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2595,6 @@ pub const Effects = struct {
|
|
|
2521
2595
|
} else {
|
|
2522
2596
|
// checkHit already checks for Invulnerable
|
|
2523
2597
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2598
|
}
|
|
2526
2599
|
|
|
2527
2600
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2708,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2708
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2709
|
}
|
|
2637
2710
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2711
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2712
|
var log = options.log;
|
|
2640
2713
|
var side = battle.side(who);
|
|
2641
2714
|
var volatiles = &side.active.volatiles;
|
|
2642
2715
|
const ident = battle.active(who);
|
|
2643
2716
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2717
|
if (volatiles.disable_move != 0) {
|
|
2647
2718
|
volatiles.disable_move = 0;
|
|
2648
2719
|
volatiles.disable_duration = 0;
|
|
2720
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2721
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2722
|
}
|
|
2651
2723
|
if (volatiles.Confusion) {
|
|
2652
2724
|
// volatiles.confusion is left unchanged
|
|
2653
2725
|
volatiles.Confusion = false;
|
|
2726
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2727
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2728
|
}
|
|
2656
2729
|
if (volatiles.Mist) {
|
|
@@ -2685,6 +2758,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2758
|
}
|
|
2686
2759
|
}
|
|
2687
2760
|
|
|
2761
|
+
fn decrement(
|
|
2762
|
+
comptime field: chance.Action.Field,
|
|
2763
|
+
player: Player,
|
|
2764
|
+
options: anytype,
|
|
2765
|
+
n: anytype,
|
|
2766
|
+
) @TypeOf(n) {
|
|
2767
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2768
|
+
.started, .ended => 0,
|
|
2769
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2770
|
+
else => unreachable,
|
|
2771
|
+
} else n - 1;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2688
2774
|
pub const Rolls = struct {
|
|
2689
2775
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2776
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2700,7 +2786,13 @@ pub const Rolls = struct {
|
|
|
2700
2786
|
return p1;
|
|
2701
2787
|
}
|
|
2702
2788
|
|
|
2703
|
-
fn criticalHit(
|
|
2789
|
+
fn criticalHit(
|
|
2790
|
+
battle: anytype,
|
|
2791
|
+
player: Player,
|
|
2792
|
+
rate: u8,
|
|
2793
|
+
spurious: bool,
|
|
2794
|
+
options: anytype,
|
|
2795
|
+
) !bool {
|
|
2704
2796
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2705
2797
|
val == .true
|
|
2706
2798
|
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
@@ -2710,7 +2802,7 @@ pub const Rolls = struct {
|
|
|
2710
2802
|
else
|
|
2711
2803
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2712
2804
|
|
|
2713
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2805
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2714
2806
|
return crit;
|
|
2715
2807
|
}
|
|
2716
2808
|
|
|
@@ -2732,17 +2824,19 @@ pub const Rolls = struct {
|
|
|
2732
2824
|
return roll;
|
|
2733
2825
|
}
|
|
2734
2826
|
|
|
2735
|
-
|
|
2827
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2828
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2829
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2736
2830
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2737
2831
|
val == .true
|
|
2738
2832
|
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2739
|
-
battle.rng.hit(player,
|
|
2833
|
+
battle.rng.hit(player, acc)
|
|
2740
2834
|
else if (showdown)
|
|
2741
|
-
battle.rng.chance(
|
|
2835
|
+
battle.rng.chance(uN, acc, 256)
|
|
2742
2836
|
else
|
|
2743
|
-
battle.rng.next() <
|
|
2837
|
+
battle.rng.next() < acc;
|
|
2744
2838
|
|
|
2745
|
-
options.chance.hit(ok,
|
|
2839
|
+
try options.chance.hit(player, ok, acc);
|
|
2746
2840
|
return ok;
|
|
2747
2841
|
}
|
|
2748
2842
|
|
|
@@ -2874,9 +2968,10 @@ pub const Rolls = struct {
|
|
|
2874
2968
|
}
|
|
2875
2969
|
|
|
2876
2970
|
fn sleepDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2877
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2878
|
-
@intCast(val)
|
|
2879
|
-
else
|
|
2971
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2972
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2973
|
+
else
|
|
2974
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2880
2975
|
battle.rng.sleepDuration(player)
|
|
2881
2976
|
else if (showdown)
|
|
2882
2977
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2888,14 +2983,15 @@ pub const Rolls = struct {
|
|
|
2888
2983
|
};
|
|
2889
2984
|
|
|
2890
2985
|
assert(duration >= 1 and duration <= 7);
|
|
2891
|
-
options.chance.duration(
|
|
2986
|
+
options.chance.duration(player, duration);
|
|
2892
2987
|
return duration;
|
|
2893
2988
|
}
|
|
2894
2989
|
|
|
2895
2990
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2896
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
|
-
val
|
|
2898
|
-
else
|
|
2991
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2992
|
+
if (val >= 1 and val <= 8) val else null
|
|
2993
|
+
else
|
|
2994
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2899
2995
|
battle.rng.disableDuration(player)
|
|
2900
2996
|
else if (showdown)
|
|
2901
2997
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2903,29 +2999,31 @@ pub const Rolls = struct {
|
|
|
2903
2999
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2904
3000
|
|
|
2905
3001
|
assert(duration >= 1 and duration <= 8);
|
|
2906
|
-
options.chance.duration(
|
|
3002
|
+
options.chance.duration(player, duration);
|
|
2907
3003
|
return duration;
|
|
2908
3004
|
}
|
|
2909
3005
|
|
|
2910
|
-
fn confusionDuration(battle: anytype, player: Player,
|
|
2911
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2912
|
-
@intCast(val)
|
|
2913
|
-
else
|
|
2914
|
-
battle.rng
|
|
3006
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
3007
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3008
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3009
|
+
else
|
|
3010
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3011
|
+
battle.rng.confusionDuration(player)
|
|
2915
3012
|
else if (showdown)
|
|
2916
3013
|
battle.rng.range(u3, 2, 6)
|
|
2917
3014
|
else
|
|
2918
3015
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2919
3016
|
|
|
2920
3017
|
assert(duration >= 2 and duration <= 5);
|
|
2921
|
-
options.chance.duration(
|
|
3018
|
+
options.chance.duration(player, duration);
|
|
2922
3019
|
return duration;
|
|
2923
3020
|
}
|
|
2924
3021
|
|
|
2925
3022
|
fn attackingDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2926
|
-
const duration: u3 = if (options.calc.overridden(player, .duration)) |val|
|
|
2927
|
-
@intCast(val)
|
|
2928
|
-
else
|
|
3023
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3024
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3025
|
+
else
|
|
3026
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2929
3027
|
battle.rng.attackingDuration(player)
|
|
2930
3028
|
else if (showdown)
|
|
2931
3029
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2933,7 +3031,7 @@ pub const Rolls = struct {
|
|
|
2933
3031
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2934
3032
|
|
|
2935
3033
|
assert(duration >= 2 and duration <= 3);
|
|
2936
|
-
options.chance.duration(
|
|
3034
|
+
options.chance.duration(player, duration);
|
|
2937
3035
|
return duration;
|
|
2938
3036
|
}
|
|
2939
3037
|
|
|
@@ -2946,9 +3044,10 @@ pub const Rolls = struct {
|
|
|
2946
3044
|
options: anytype,
|
|
2947
3045
|
) !u3 {
|
|
2948
3046
|
const roll = if (effect == .Binding) .duration else .multi_hit;
|
|
2949
|
-
const n: u3 = if (options.calc.overridden(player, roll)) |val|
|
|
2950
|
-
@intCast(val)
|
|
2951
|
-
else
|
|
3047
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3048
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3049
|
+
else
|
|
3050
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2952
3051
|
battle.rng.distribution(player)
|
|
2953
3052
|
else if (showdown)
|
|
2954
3053
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2959,7 +3058,7 @@ pub const Rolls = struct {
|
|
|
2959
3058
|
|
|
2960
3059
|
assert(n >= 2 and n <= 5);
|
|
2961
3060
|
if (effect == .Binding) {
|
|
2962
|
-
options.chance.duration(
|
|
3061
|
+
options.chance.duration(player, n);
|
|
2963
3062
|
} else {
|
|
2964
3063
|
try options.chance.multiHit(player, n);
|
|
2965
3064
|
}
|
|
@@ -2974,15 +3073,13 @@ pub const Rolls = struct {
|
|
|
2974
3073
|
options: anytype,
|
|
2975
3074
|
) !u4 {
|
|
2976
3075
|
// TODO: consider throwing error instead of rerolling?
|
|
2977
|
-
const
|
|
3076
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2978
3077
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2979
3078
|
val
|
|
2980
3079
|
else
|
|
2981
3080
|
null
|
|
2982
3081
|
else
|
|
2983
|
-
null
|
|
2984
|
-
|
|
2985
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3082
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2986
3083
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2987
3084
|
else slot: {
|
|
2988
3085
|
if (showdown) {
|
|
@@ -3048,11 +3145,11 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3048
3145
|
},
|
|
3049
3146
|
.Switch => {
|
|
3050
3147
|
const side = battle.side(player);
|
|
3051
|
-
var slot:
|
|
3148
|
+
var slot: usize = 2;
|
|
3052
3149
|
while (slot <= 6) : (slot += 1) {
|
|
3053
3150
|
const id = side.order[slot - 1];
|
|
3054
3151
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3055
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3152
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3056
3153
|
n += 1;
|
|
3057
3154
|
}
|
|
3058
3155
|
if (n == 0) {
|
|
@@ -3068,7 +3165,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3068
3165
|
const stored = side.stored();
|
|
3069
3166
|
|
|
3070
3167
|
// While players are not given any input options on the cartridge in these cases,
|
|
3071
|
-
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3168
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3072
3169
|
//
|
|
3073
3170
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3074
3171
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3076,18 +3173,18 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3076
3173
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3077
3174
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3078
3175
|
// strictly correct as the player would not have been presented the option to move or
|
|
3079
|
-
// switch at all
|
|
3176
|
+
// switch at all
|
|
3080
3177
|
if (isForced(active)) {
|
|
3081
3178
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3082
3179
|
n += 1;
|
|
3083
3180
|
return n;
|
|
3084
3181
|
}
|
|
3085
3182
|
|
|
3086
|
-
var slot:
|
|
3183
|
+
var slot: usize = 2;
|
|
3087
3184
|
while (slot <= 6) : (slot += 1) {
|
|
3088
3185
|
const id = side.order[slot - 1];
|
|
3089
3186
|
if (id == 0 or side.pokemon[id - 1].hp == 0) continue;
|
|
3090
|
-
out[n] = .{ .type = .Switch, .data = slot };
|
|
3187
|
+
out[n] = .{ .type = .Switch, .data = @intCast(slot) };
|
|
3091
3188
|
n += 1;
|
|
3092
3189
|
}
|
|
3093
3190
|
|
|
@@ -3095,7 +3192,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3095
3192
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3096
3193
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3097
3194
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3098
|
-
// move normally if sleeping/frozen/bound
|
|
3195
|
+
// move normally if sleeping/frozen/bound
|
|
3099
3196
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3100
3197
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3101
3198
|
{
|
|
@@ -3108,7 +3205,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3205
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3109
3206
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3110
3207
|
// Move will not result in forcing the subsequent use unless the user also had the
|
|
3111
|
-
// proc-ed move in their moveset) and disabling all other moves
|
|
3208
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3112
3209
|
if (limited) {
|
|
3113
3210
|
assert(showdown);
|
|
3114
3211
|
assert(side.last_selected_move != .None);
|
|
@@ -3120,7 +3217,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3120
3217
|
const struggle =
|
|
3121
3218
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disable_move == slot);
|
|
3122
3219
|
const s = if (struggle) 0 else slot;
|
|
3123
|
-
out[n] = .{ .type = .Move, .data = s };
|
|
3220
|
+
out[n] = .{ .type = .Move, .data = @intCast(s) };
|
|
3124
3221
|
n += 1;
|
|
3125
3222
|
return n;
|
|
3126
3223
|
}
|
|
@@ -3134,13 +3231,13 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3134
3231
|
if (m.id == .None) break;
|
|
3135
3232
|
if (m.pp == 0) continue;
|
|
3136
3233
|
if (active.volatiles.disable_move == slot) continue;
|
|
3137
|
-
out[n] = .{ .type = .Move, .data = slot };
|
|
3234
|
+
out[n] = .{ .type = .Move, .data = @intCast(slot) };
|
|
3138
3235
|
n += 1;
|
|
3139
3236
|
}
|
|
3140
3237
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3141
3238
|
if (n == before) {
|
|
3142
3239
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3143
|
-
if (!
|
|
3240
|
+
if (!pkmn.options.mod) {
|
|
3144
3241
|
while (slot <= 4) : (slot += 1) {
|
|
3145
3242
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3146
3243
|
}
|