@pkmn/engine 0.1.0-dev.abd68573 → 0.1.0-dev.ac652f4e
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/README.md +17 -12
- package/build/pkg/addon/browser.d.ts +3 -0
- package/build/pkg/addon/browser.js +47 -0
- package/build/pkg/addon/browser.js.map +1 -0
- package/build/pkg/addon/node.d.ts +3 -0
- package/build/pkg/addon/node.js +110 -0
- package/build/pkg/addon/node.js.map +1 -0
- package/build/pkg/addon/wasm.d.ts +2 -0
- package/build/pkg/addon/wasm.js +53 -0
- package/build/pkg/addon/wasm.js.map +1 -0
- package/build/pkg/addon.d.ts +25 -3
- package/build/pkg/addon.js +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 +52 -76
- package/build/tools/debug.js.map +1 -1
- package/build/tools/display/debug.jsx +152 -0
- package/build/tools/display/debug.jsx.map +1 -0
- package/build/tools/display/index.js +152 -0
- package/build/tools/display/index.js.map +1 -0
- package/build/tools/display/ui.jsx +359 -0
- package/build/tools/display/ui.jsx.map +1 -0
- package/build/tools/display/util.js +33 -0
- package/build/tools/display/util.js.map +1 -0
- package/build.zig +307 -78
- package/package.json +54 -135
- package/src/bin/install-pkmn-engine +10 -7
- package/src/bin/pkmn-debug +1 -1
- package/src/lib/bindings/c.zig +255 -0
- package/src/lib/bindings/node.zig +121 -0
- package/src/lib/bindings/wasm.zig +59 -0
- package/src/lib/common/array.zig +77 -0
- package/src/lib/common/data.zig +8 -8
- package/src/lib/common/debug.zig +39 -8
- package/src/lib/common/js.zig +4 -2
- package/src/lib/common/optional.zig +13 -9
- package/src/lib/common/options.zig +6 -0
- package/src/lib/common/protocol.zig +45 -44
- package/src/lib/common/rational.zig +323 -158
- package/src/lib/common/rng.zig +5 -7
- package/src/lib/common/util.zig +60 -1
- package/src/lib/gen1/calc.zig +693 -157
- package/src/lib/gen1/chance.zig +415 -291
- 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 +1 -2
- package/src/lib/gen1/data.zig +38 -41
- package/src/lib/gen1/helpers.zig +64 -540
- package/src/lib/gen1/mechanics.zig +332 -244
- package/src/lib/gen2/calc.zig +37 -27
- package/src/lib/gen2/chance.zig +90 -503
- package/src/lib/gen2/data/items.zig +1 -3
- package/src/lib/gen2/data/moves.zig +2 -4
- package/src/lib/gen2/data/species.zig +1 -3
- package/src/lib/gen2/data/types.zig +1 -2
- package/src/lib/gen2/data.zig +26 -29
- package/src/lib/gen2/generated.zig +18 -23
- package/src/lib/gen2/helpers.zig +46 -25
- package/src/lib/gen2/mechanics.zig +88 -115
- package/src/lib/node.zig +3 -116
- package/src/lib/pkmn.zig +14 -7
- package/src/lib/wasm.zig +13 -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
|
-
try options.chance.attacking(player, volatiles.attacks);
|
|
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);
|
|
683
701
|
|
|
684
|
-
if (
|
|
702
|
+
if (volatiles.attacks == 0) {
|
|
703
|
+
const overwritten = volatiles.Confusion;
|
|
704
|
+
|
|
705
|
+
volatiles.Thrashing = false;
|
|
706
|
+
volatiles.Confusion = true;
|
|
707
|
+
volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
708
|
+
|
|
709
|
+
// On Pokémon Showdown this could leak information to the opponent that thrashing has
|
|
710
|
+
// ended (the engine's protocol is always from the omniscient perspective and clients
|
|
711
|
+
// can be expected to recognize this pattern and pass the information only to the
|
|
712
|
+
// correct player)
|
|
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
|
|
@@ -852,13 +885,13 @@ fn doMove(
|
|
|
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;
|
|
@@ -994,32 +1048,30 @@ fn doMove(
|
|
|
994
1048
|
try Effects.poison(battle, player, Move.get(.PoisonSting), options);
|
|
995
1049
|
}
|
|
996
1050
|
try log.hitcount(.{ battle.active(player.foe()), hit });
|
|
997
|
-
} else if (showdown) {
|
|
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);
|
|
@@ -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
|
|
|
@@ -1407,7 +1477,7 @@ fn moveHit(
|
|
|
1407
1477
|
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1408
1478
|
|
|
1409
1479
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1410
|
-
break :miss !Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1480
|
+
break :miss !try Rolls.hit(battle, player, @intCast(accuracy), options);
|
|
1411
1481
|
};
|
|
1412
1482
|
|
|
1413
1483
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1418,13 +1488,17 @@ fn moveHit(
|
|
|
1418
1488
|
|
|
1419
1489
|
if (!miss) return true;
|
|
1420
1490
|
if (!showdown or !foe.active.volatiles.Invulnerable) battle.last_damage = 0;
|
|
1491
|
+
|
|
1421
1492
|
side.active.volatiles.Binding = false;
|
|
1493
|
+
options.chance.observe(.binding, player, .None);
|
|
1494
|
+
|
|
1422
1495
|
return false;
|
|
1423
1496
|
}
|
|
1424
1497
|
|
|
1425
1498
|
fn checkFaint(
|
|
1426
1499
|
battle: anytype,
|
|
1427
1500
|
player: Player,
|
|
1501
|
+
cap: bool,
|
|
1428
1502
|
options: anytype,
|
|
1429
1503
|
) @TypeOf(options.log).Error!?Result {
|
|
1430
1504
|
const side = battle.side(player);
|
|
@@ -1437,8 +1511,8 @@ fn checkFaint(
|
|
|
1437
1511
|
const foe_out = foe_fainted and findFirstAlive(foe) == 0;
|
|
1438
1512
|
const more = player_out or foe_out;
|
|
1439
1513
|
|
|
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;
|
|
1514
|
+
if (try faint(battle, player, !(more or foe_fainted), cap, options)) |r| return r;
|
|
1515
|
+
if (foe_fainted) if (try faint(battle, player.foe(), !more, false, options)) |r| return r;
|
|
1442
1516
|
|
|
1443
1517
|
assert(!side.active.volatiles.MultiHit);
|
|
1444
1518
|
assert(!foe.active.volatiles.MultiHit);
|
|
@@ -1459,7 +1533,7 @@ fn checkFaint(
|
|
|
1459
1533
|
return .{ .p1 = foe_choice, .p2 = .Switch };
|
|
1460
1534
|
}
|
|
1461
1535
|
|
|
1462
|
-
fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result {
|
|
1536
|
+
fn faint(battle: anytype, player: Player, done: bool, cap: bool, options: anytype) !?Result {
|
|
1463
1537
|
var side = battle.side(player);
|
|
1464
1538
|
var foe = battle.foe(player);
|
|
1465
1539
|
assert(side.stored().hp == 0);
|
|
@@ -1468,7 +1542,7 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1468
1542
|
assert(!foe_volatiles.MultiHit);
|
|
1469
1543
|
if (foe_volatiles.Bide) {
|
|
1470
1544
|
assert(!foe_volatiles.Thrashing and !foe_volatiles.Rage);
|
|
1471
|
-
foe_volatiles.state = if (
|
|
1545
|
+
foe_volatiles.state = if (pkmn.options.mod) 0 else foe_volatiles.state & 255;
|
|
1472
1546
|
if (foe_volatiles.state != 0) return Result.Error;
|
|
1473
1547
|
}
|
|
1474
1548
|
|
|
@@ -1493,7 +1567,9 @@ fn faint(battle: anytype, player: Player, done: bool, options: anytype) !?Result
|
|
|
1493
1567
|
}
|
|
1494
1568
|
|
|
1495
1569
|
try options.log.faint(.{ battle.active(player), done });
|
|
1496
|
-
|
|
1570
|
+
// We can only safely set capped if the faint occured as the direct result of an opponent's
|
|
1571
|
+
// move (as opposed to due to residual damage, recoil, Explosion, etc)
|
|
1572
|
+
if (cap) options.calc.capped(player);
|
|
1497
1573
|
return null;
|
|
1498
1574
|
}
|
|
1499
1575
|
|
|
@@ -1557,12 +1633,12 @@ fn endTurn(battle: anytype, options: anytype) @TypeOf(options.log).Error!Result
|
|
|
1557
1633
|
|
|
1558
1634
|
battle.turn += 1;
|
|
1559
1635
|
|
|
1560
|
-
if (
|
|
1636
|
+
if (pkmn.options.mod and pkmn.options.ebc and checkEBC(battle)) {
|
|
1561
1637
|
try options.log.tie(.{});
|
|
1562
1638
|
return Result.Tie;
|
|
1563
1639
|
}
|
|
1564
1640
|
|
|
1565
|
-
if (
|
|
1641
|
+
if (pkmn.options.mod and battle.turn >= 1000) {
|
|
1566
1642
|
try options.log.tie(.{});
|
|
1567
1643
|
return Result.Tie;
|
|
1568
1644
|
} else if (battle.turn >= 65535) {
|
|
@@ -1653,17 +1729,6 @@ fn buildRage(battle: anytype, who: Player, options: anytype) !void {
|
|
|
1653
1729
|
}
|
|
1654
1730
|
}
|
|
1655
1731
|
|
|
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
1732
|
fn onBegin(
|
|
1668
1733
|
battle: anytype,
|
|
1669
1734
|
player: Player,
|
|
@@ -1750,8 +1815,10 @@ pub const Effects = struct {
|
|
|
1750
1815
|
|
|
1751
1816
|
side.active.volatiles.Bide = true;
|
|
1752
1817
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1818
|
+
|
|
1753
1819
|
side.active.volatiles.state = 0;
|
|
1754
1820
|
side.active.volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
1821
|
+
options.chance.observe(.attacking, player, .started);
|
|
1755
1822
|
|
|
1756
1823
|
try options.log.start(.{ battle.active(player), .Bide });
|
|
1757
1824
|
}
|
|
@@ -1813,12 +1880,14 @@ pub const Effects = struct {
|
|
|
1813
1880
|
if (sub) return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
1814
1881
|
if (!try checkHit(battle, player, move, options)) return;
|
|
1815
1882
|
}
|
|
1816
|
-
try options.chance.commit(player, .hit);
|
|
1817
1883
|
}
|
|
1818
1884
|
|
|
1819
1885
|
if (foe.active.volatiles.Confusion) return;
|
|
1820
1886
|
foe.active.volatiles.Confusion = true;
|
|
1821
|
-
|
|
1887
|
+
|
|
1888
|
+
foe.active.volatiles.confusion = Rolls.confusionDuration(battle, player, options);
|
|
1889
|
+
options.chance.observe(.confusion, player.foe(), .started);
|
|
1890
|
+
options.calc.confusion(player.foe());
|
|
1822
1891
|
|
|
1823
1892
|
try options.log.start(.{ battle.active(player.foe()), .Confusion });
|
|
1824
1893
|
}
|
|
@@ -1846,10 +1915,7 @@ pub const Effects = struct {
|
|
|
1846
1915
|
const foe_ident = battle.active(player.foe());
|
|
1847
1916
|
|
|
1848
1917
|
// 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
|
-
}
|
|
1918
|
+
if (!showdown and !try checkHit(battle, player, move, options)) return null;
|
|
1853
1919
|
|
|
1854
1920
|
if (volatiles.disable_move != 0) {
|
|
1855
1921
|
try options.log.fail(.{ foe_ident, .None });
|
|
@@ -1867,18 +1933,18 @@ pub const Effects = struct {
|
|
|
1867
1933
|
|
|
1868
1934
|
// Technically this is still considered simply a "miss" on the cartridge,
|
|
1869
1935
|
// but diverging from Pokémon Showdown here would mostly just be pedantic
|
|
1870
|
-
if (n == 0) {
|
|
1936
|
+
if (n == 0 or (pkmn.options.mod and err)) {
|
|
1871
1937
|
try options.log.fail(.{ foe_ident, .None });
|
|
1872
1938
|
return null;
|
|
1873
1939
|
} else if (err) {
|
|
1874
1940
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
1875
|
-
assert(!showdown);
|
|
1876
1941
|
return Result.Error;
|
|
1877
1942
|
}
|
|
1878
1943
|
|
|
1879
1944
|
volatiles.disable_move =
|
|
1880
1945
|
@intCast(try Rolls.moveSlot(battle, player, &foe.active.moves, n, options));
|
|
1881
1946
|
volatiles.disable_duration = Rolls.disableDuration(battle, player, options);
|
|
1947
|
+
options.chance.observe(.disable, player.foe(), .started);
|
|
1882
1948
|
|
|
1883
1949
|
const id = foe.active.move(volatiles.disable_move).id;
|
|
1884
1950
|
try options.log.start(.{ foe_ident, .Disable, id });
|
|
@@ -1949,7 +2015,7 @@ pub const Effects = struct {
|
|
|
1949
2015
|
if (foe.active.types.includes(move.type)) return;
|
|
1950
2016
|
if (!try Rolls.secondaryChance(battle, player, true, options)) return;
|
|
1951
2017
|
// Freeze Clause Mod
|
|
1952
|
-
if (
|
|
2018
|
+
if (pkmn.options.mod) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1953
2019
|
|
|
1954
2020
|
foe_stored.status = Status.init(.FRZ);
|
|
1955
2021
|
// GLITCH: Hyper Beam recharging status is not cleared
|
|
@@ -1985,22 +2051,24 @@ pub const Effects = struct {
|
|
|
1985
2051
|
if (p != player and Status.any(s.stored().status)) {
|
|
1986
2052
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
1987
2053
|
s.stored().status = 0;
|
|
2054
|
+
options.chance.observe(.sleep, p, .None);
|
|
1988
2055
|
} else if (showdown and s.stored().status == Status.TOX) {
|
|
1989
2056
|
s.stored().status = Status.init(.PSN);
|
|
1990
2057
|
try log.status(.{ battle.active(p), s.stored().status, .None });
|
|
1991
2058
|
}
|
|
1992
|
-
try clearVolatiles(battle, p,
|
|
2059
|
+
try clearVolatiles(battle, p, options);
|
|
1993
2060
|
}
|
|
1994
2061
|
} else {
|
|
1995
2062
|
if (Status.any(foe_stored.status)) {
|
|
1996
2063
|
if (Status.is(foe_stored.status, .FRZ) or Status.is(foe_stored.status, .SLP)) {
|
|
1997
2064
|
foe.last_selected_move = .SKIP_TURN;
|
|
2065
|
+
options.chance.observe(.sleep, player.foe(), .None);
|
|
1998
2066
|
}
|
|
1999
2067
|
try log.curestatus(.{ foe_ident, foe_stored.status, .Silent });
|
|
2000
2068
|
foe_stored.status = 0;
|
|
2001
2069
|
}
|
|
2002
|
-
try clearVolatiles(battle, player,
|
|
2003
|
-
try clearVolatiles(battle, player.foe(),
|
|
2070
|
+
try clearVolatiles(battle, player, options);
|
|
2071
|
+
try clearVolatiles(battle, player.foe(), options);
|
|
2004
2072
|
}
|
|
2005
2073
|
}
|
|
2006
2074
|
|
|
@@ -2051,7 +2119,6 @@ pub const Effects = struct {
|
|
|
2051
2119
|
}
|
|
2052
2120
|
}
|
|
2053
2121
|
|
|
2054
|
-
try options.chance.commit(player, .hit);
|
|
2055
2122
|
foe.active.volatiles.LeechSeed = true;
|
|
2056
2123
|
|
|
2057
2124
|
try options.log.start(.{ battle.active(player.foe()), .LeechSeed });
|
|
@@ -2092,12 +2159,10 @@ pub const Effects = struct {
|
|
|
2092
2159
|
if (!has_mimic) {
|
|
2093
2160
|
// Invulnerable foes or 1/256 miss can trigger |-miss| instead of |-fail|
|
|
2094
2161
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2095
|
-
try options.chance.commit(player, .hit);
|
|
2096
2162
|
return options.log.fail(.{ battle.active(player.foe()), .None });
|
|
2097
2163
|
}
|
|
2098
2164
|
}
|
|
2099
2165
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2100
|
-
try options.chance.commit(player, .hit);
|
|
2101
2166
|
|
|
2102
2167
|
const rslot = try Rolls.moveSlot(battle, player, &foe.active.moves, 0, options);
|
|
2103
2168
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
@@ -2151,7 +2216,6 @@ pub const Effects = struct {
|
|
|
2151
2216
|
if (immune) return log.immune(.{ foe_ident, .None });
|
|
2152
2217
|
if (!try checkHit(battle, player, move, options)) return;
|
|
2153
2218
|
}
|
|
2154
|
-
try options.chance.commit(player, .hit);
|
|
2155
2219
|
|
|
2156
2220
|
foe_stored.status = Status.init(.PAR);
|
|
2157
2221
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2219,7 +2283,6 @@ pub const Effects = struct {
|
|
|
2219
2283
|
|
|
2220
2284
|
if (move.effect == .Poison) {
|
|
2221
2285
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2222
|
-
try options.chance.commit(player, .hit);
|
|
2223
2286
|
} else {
|
|
2224
2287
|
if (!try Rolls.poisonChance(battle, player, move.effect == .PoisonChance1, options)) {
|
|
2225
2288
|
return;
|
|
@@ -2288,13 +2351,12 @@ pub const Effects = struct {
|
|
|
2288
2351
|
if (Status.is(foe_stored.status, .SLP)) Fail.Sleep else Fail.None,
|
|
2289
2352
|
});
|
|
2290
2353
|
}
|
|
2291
|
-
// If
|
|
2354
|
+
// If moveHit in doMove didn't return true Pokémon Showdown wouldn't be in here
|
|
2292
2355
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2293
|
-
try options.chance.commit(player, .hit);
|
|
2294
2356
|
}
|
|
2295
2357
|
|
|
2296
2358
|
// Sleep Clause Mod
|
|
2297
|
-
if (
|
|
2359
|
+
if (pkmn.options.mod) {
|
|
2298
2360
|
for (foe.pokemon) |p| {
|
|
2299
2361
|
if (Status.is(p.status, .SLP) and !Status.is(p.status, .EXT)) return;
|
|
2300
2362
|
}
|
|
@@ -2302,6 +2364,8 @@ pub const Effects = struct {
|
|
|
2302
2364
|
foe.active.volatiles.Recharging = false;
|
|
2303
2365
|
|
|
2304
2366
|
foe_stored.status = Status.slp(Rolls.sleepDuration(battle, player, options));
|
|
2367
|
+
options.chance.observe(.sleep, player.foe(), .started);
|
|
2368
|
+
|
|
2305
2369
|
const last = battle.side(player).last_selected_move;
|
|
2306
2370
|
try options.log.status(.{ foe_ident, foe_stored.status, .From, last });
|
|
2307
2371
|
}
|
|
@@ -2343,9 +2407,7 @@ pub const Effects = struct {
|
|
|
2343
2407
|
// Whirlwind/Roar should not roll to hit/reset damage but Pokémon Showdown does anyway
|
|
2344
2408
|
if (showdown) {
|
|
2345
2409
|
if (battle.side(player).last_selected_move == .Teleport) return;
|
|
2346
|
-
|
|
2347
|
-
try options.chance.commit(player, .hit);
|
|
2348
|
-
}
|
|
2410
|
+
_ = try checkHit(battle, player, move, options);
|
|
2349
2411
|
battle.last_damage = 0;
|
|
2350
2412
|
} else {
|
|
2351
2413
|
try options.log.fail(.{ battle.active(player), .None });
|
|
@@ -2360,6 +2422,8 @@ pub const Effects = struct {
|
|
|
2360
2422
|
|
|
2361
2423
|
volatiles.Thrashing = true;
|
|
2362
2424
|
volatiles.attacks = Rolls.attackingDuration(battle, player, options);
|
|
2425
|
+
|
|
2426
|
+
options.chance.observe(.attacking, player, .started);
|
|
2363
2427
|
}
|
|
2364
2428
|
|
|
2365
2429
|
fn transform(battle: anytype, player: Player, options: anytype) !void {
|
|
@@ -2398,13 +2462,14 @@ pub const Effects = struct {
|
|
|
2398
2462
|
if (side.active.volatiles.Binding) return;
|
|
2399
2463
|
side.active.volatiles.Binding = true;
|
|
2400
2464
|
// GLITCH: Hyper Beam automatic selection glitch if Recharging gets cleared on miss
|
|
2401
|
-
// (Pokémon Showdown
|
|
2465
|
+
// (Pokémon Showdown clears Recharging in onTryMove instead)
|
|
2402
2466
|
if (!showdown) {
|
|
2403
2467
|
foe.active.volatiles.Recharging = false;
|
|
2404
2468
|
} else if (foe.stored().hp == 0) return if (!rewrap) battle.rng.advance(1);
|
|
2405
2469
|
|
|
2406
2470
|
side.active.volatiles.attacks =
|
|
2407
2471
|
try Rolls.distribution(battle, .Binding, player, options) - 1;
|
|
2472
|
+
options.chance.observe(.binding, player, .started);
|
|
2408
2473
|
}
|
|
2409
2474
|
|
|
2410
2475
|
fn boost(battle: anytype, player: Player, move: Move.Data, options: anytype) !void {
|
|
@@ -2521,7 +2586,6 @@ pub const Effects = struct {
|
|
|
2521
2586
|
} else {
|
|
2522
2587
|
// checkHit already checks for Invulnerable
|
|
2523
2588
|
if (!showdown and !try checkHit(battle, player, move, options)) return;
|
|
2524
|
-
try options.chance.commit(player, .hit);
|
|
2525
2589
|
}
|
|
2526
2590
|
|
|
2527
2591
|
var stats = &foe.active.stats;
|
|
@@ -2635,22 +2699,22 @@ fn isForced(active: anytype) bool {
|
|
|
2635
2699
|
active.volatiles.Thrashing or active.volatiles.Charging;
|
|
2636
2700
|
}
|
|
2637
2701
|
|
|
2638
|
-
fn clearVolatiles(battle: anytype, who: Player,
|
|
2702
|
+
fn clearVolatiles(battle: anytype, who: Player, options: anytype) !void {
|
|
2639
2703
|
var log = options.log;
|
|
2640
2704
|
var side = battle.side(who);
|
|
2641
2705
|
var volatiles = &side.active.volatiles;
|
|
2642
2706
|
const ident = battle.active(who);
|
|
2643
2707
|
|
|
2644
|
-
options.chance.clearDurations(who, clear);
|
|
2645
|
-
|
|
2646
2708
|
if (volatiles.disable_move != 0) {
|
|
2647
2709
|
volatiles.disable_move = 0;
|
|
2648
2710
|
volatiles.disable_duration = 0;
|
|
2711
|
+
options.chance.observe(.disable, who, .None);
|
|
2649
2712
|
try log.end(.{ ident, .DisableSilent });
|
|
2650
2713
|
}
|
|
2651
2714
|
if (volatiles.Confusion) {
|
|
2652
2715
|
// volatiles.confusion is left unchanged
|
|
2653
2716
|
volatiles.Confusion = false;
|
|
2717
|
+
options.chance.observe(.confusion, who, .None);
|
|
2654
2718
|
try log.end(.{ ident, .ConfusionSilent });
|
|
2655
2719
|
}
|
|
2656
2720
|
if (volatiles.Mist) {
|
|
@@ -2685,6 +2749,19 @@ fn clearVolatiles(battle: anytype, who: Player, clear: bool, options: anytype) !
|
|
|
2685
2749
|
}
|
|
2686
2750
|
}
|
|
2687
2751
|
|
|
2752
|
+
fn decrement(
|
|
2753
|
+
comptime field: chance.Action.Field,
|
|
2754
|
+
player: Player,
|
|
2755
|
+
options: anytype,
|
|
2756
|
+
n: anytype,
|
|
2757
|
+
) @TypeOf(n) {
|
|
2758
|
+
return if (options.calc.overridden(player, field)) |obs| switch (obs) {
|
|
2759
|
+
.started, .ended => 0,
|
|
2760
|
+
.continuing => if (n > 1) n - 1 else n,
|
|
2761
|
+
else => unreachable,
|
|
2762
|
+
} else n - 1;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2688
2765
|
pub const Rolls = struct {
|
|
2689
2766
|
fn speedTie(battle: anytype, options: anytype) !bool {
|
|
2690
2767
|
const p1 = if (options.calc.overridden(.P1, .speed_tie)) |player|
|
|
@@ -2700,7 +2777,13 @@ pub const Rolls = struct {
|
|
|
2700
2777
|
return p1;
|
|
2701
2778
|
}
|
|
2702
2779
|
|
|
2703
|
-
fn criticalHit(
|
|
2780
|
+
fn criticalHit(
|
|
2781
|
+
battle: anytype,
|
|
2782
|
+
player: Player,
|
|
2783
|
+
rate: u8,
|
|
2784
|
+
spurious: bool,
|
|
2785
|
+
options: anytype,
|
|
2786
|
+
) !bool {
|
|
2704
2787
|
const crit = if (options.calc.overridden(player, .critical_hit)) |val|
|
|
2705
2788
|
val == .true
|
|
2706
2789
|
else if (@hasDecl(@TypeOf(battle.rng), "criticalHit"))
|
|
@@ -2710,7 +2793,7 @@ pub const Rolls = struct {
|
|
|
2710
2793
|
else
|
|
2711
2794
|
std.math.rotl(u8, battle.rng.next(), 3) < rate;
|
|
2712
2795
|
|
|
2713
|
-
try options.chance.criticalHit(player, crit, rate);
|
|
2796
|
+
if (!spurious) try options.chance.criticalHit(player, crit, rate);
|
|
2714
2797
|
return crit;
|
|
2715
2798
|
}
|
|
2716
2799
|
|
|
@@ -2732,17 +2815,19 @@ pub const Rolls = struct {
|
|
|
2732
2815
|
return roll;
|
|
2733
2816
|
}
|
|
2734
2817
|
|
|
2735
|
-
|
|
2818
|
+
const uN = if (pkmn.options.miss) u8 else u9;
|
|
2819
|
+
fn hit(battle: anytype, player: Player, accuracy: u8, options: anytype) !bool {
|
|
2820
|
+
const acc: uN = if (pkmn.options.miss) accuracy else @as(uN, accuracy) + 1;
|
|
2736
2821
|
const ok = if (options.calc.overridden(player, .hit)) |val|
|
|
2737
2822
|
val == .true
|
|
2738
2823
|
else if (@hasDecl(@TypeOf(battle.rng), "hit"))
|
|
2739
|
-
battle.rng.hit(player,
|
|
2824
|
+
battle.rng.hit(player, acc)
|
|
2740
2825
|
else if (showdown)
|
|
2741
|
-
battle.rng.chance(
|
|
2826
|
+
battle.rng.chance(uN, acc, 256)
|
|
2742
2827
|
else
|
|
2743
|
-
battle.rng.next() <
|
|
2828
|
+
battle.rng.next() < acc;
|
|
2744
2829
|
|
|
2745
|
-
options.chance.hit(ok,
|
|
2830
|
+
try options.chance.hit(player, ok, acc);
|
|
2746
2831
|
return ok;
|
|
2747
2832
|
}
|
|
2748
2833
|
|
|
@@ -2874,9 +2959,10 @@ pub const Rolls = struct {
|
|
|
2874
2959
|
}
|
|
2875
2960
|
|
|
2876
2961
|
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
|
|
2962
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2963
|
+
if (val >= 1 and val <= 7) @as(u3, @intCast(val)) else null
|
|
2964
|
+
else
|
|
2965
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "sleepDuration"))
|
|
2880
2966
|
battle.rng.sleepDuration(player)
|
|
2881
2967
|
else if (showdown)
|
|
2882
2968
|
battle.rng.range(u3, 1, 8)
|
|
@@ -2888,14 +2974,15 @@ pub const Rolls = struct {
|
|
|
2888
2974
|
};
|
|
2889
2975
|
|
|
2890
2976
|
assert(duration >= 1 and duration <= 7);
|
|
2891
|
-
options.chance.duration(
|
|
2977
|
+
options.chance.duration(player, duration);
|
|
2892
2978
|
return duration;
|
|
2893
2979
|
}
|
|
2894
2980
|
|
|
2895
2981
|
fn disableDuration(battle: anytype, player: Player, options: anytype) u4 {
|
|
2896
|
-
const duration: u4 = if (options.calc.overridden(player, .duration)) |val|
|
|
2897
|
-
val
|
|
2898
|
-
else
|
|
2982
|
+
const duration: u4 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2983
|
+
if (val >= 1 and val <= 8) val else null
|
|
2984
|
+
else
|
|
2985
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "disableDuration"))
|
|
2899
2986
|
battle.rng.disableDuration(player)
|
|
2900
2987
|
else if (showdown)
|
|
2901
2988
|
battle.rng.range(u4, 1, 9)
|
|
@@ -2903,29 +2990,31 @@ pub const Rolls = struct {
|
|
|
2903
2990
|
@intCast((battle.rng.next() & 7) + 1);
|
|
2904
2991
|
|
|
2905
2992
|
assert(duration >= 1 and duration <= 8);
|
|
2906
|
-
options.chance.duration(
|
|
2993
|
+
options.chance.duration(player, duration);
|
|
2907
2994
|
return duration;
|
|
2908
2995
|
}
|
|
2909
2996
|
|
|
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
|
|
2997
|
+
fn confusionDuration(battle: anytype, player: Player, options: anytype) u3 {
|
|
2998
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
2999
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3000
|
+
else
|
|
3001
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "confusionDuration"))
|
|
3002
|
+
battle.rng.confusionDuration(player)
|
|
2915
3003
|
else if (showdown)
|
|
2916
3004
|
battle.rng.range(u3, 2, 6)
|
|
2917
3005
|
else
|
|
2918
3006
|
@intCast((battle.rng.next() & 3) + 2);
|
|
2919
3007
|
|
|
2920
3008
|
assert(duration >= 2 and duration <= 5);
|
|
2921
|
-
options.chance.duration(
|
|
3009
|
+
options.chance.duration(player, duration);
|
|
2922
3010
|
return duration;
|
|
2923
3011
|
}
|
|
2924
3012
|
|
|
2925
3013
|
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
|
|
3014
|
+
const duration: u3 = (if (options.calc.overridden(player, .duration)) |val|
|
|
3015
|
+
if (val >= 2 and val <= 3) @as(u3, @intCast(val)) else null
|
|
3016
|
+
else
|
|
3017
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "attackingDuration"))
|
|
2929
3018
|
battle.rng.attackingDuration(player)
|
|
2930
3019
|
else if (showdown)
|
|
2931
3020
|
battle.rng.range(u3, 2, 4)
|
|
@@ -2933,7 +3022,7 @@ pub const Rolls = struct {
|
|
|
2933
3022
|
@intCast((battle.rng.next() & 1) + 2);
|
|
2934
3023
|
|
|
2935
3024
|
assert(duration >= 2 and duration <= 3);
|
|
2936
|
-
options.chance.duration(
|
|
3025
|
+
options.chance.duration(player, duration);
|
|
2937
3026
|
return duration;
|
|
2938
3027
|
}
|
|
2939
3028
|
|
|
@@ -2946,9 +3035,10 @@ pub const Rolls = struct {
|
|
|
2946
3035
|
options: anytype,
|
|
2947
3036
|
) !u3 {
|
|
2948
3037
|
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
|
|
3038
|
+
const n: u3 = (if (options.calc.overridden(player, roll)) |val|
|
|
3039
|
+
if (val >= 2 and val <= 5) @as(u3, @intCast(val)) else null
|
|
3040
|
+
else
|
|
3041
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "distribution"))
|
|
2952
3042
|
battle.rng.distribution(player)
|
|
2953
3043
|
else if (showdown)
|
|
2954
3044
|
DISTRIBUTION[battle.rng.range(u3, 0, DISTRIBUTION.len)]
|
|
@@ -2959,7 +3049,7 @@ pub const Rolls = struct {
|
|
|
2959
3049
|
|
|
2960
3050
|
assert(n >= 2 and n <= 5);
|
|
2961
3051
|
if (effect == .Binding) {
|
|
2962
|
-
options.chance.duration(
|
|
3052
|
+
options.chance.duration(player, n);
|
|
2963
3053
|
} else {
|
|
2964
3054
|
try options.chance.multiHit(player, n);
|
|
2965
3055
|
}
|
|
@@ -2974,15 +3064,13 @@ pub const Rolls = struct {
|
|
|
2974
3064
|
options: anytype,
|
|
2975
3065
|
) !u4 {
|
|
2976
3066
|
// TODO: consider throwing error instead of rerolling?
|
|
2977
|
-
const
|
|
3067
|
+
const slot: u4 = (if (options.calc.overridden(player, .move_slot)) |val|
|
|
2978
3068
|
if (moves[val - 1].id != .None and (check_pp == 0 or moves[val - 1].pp > 0))
|
|
2979
3069
|
val
|
|
2980
3070
|
else
|
|
2981
3071
|
null
|
|
2982
3072
|
else
|
|
2983
|
-
null
|
|
2984
|
-
|
|
2985
|
-
const slot: u4 = overridden orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
3073
|
+
null) orelse if (@hasDecl(@TypeOf(battle.rng), "moveSlot"))
|
|
2986
3074
|
battle.rng.moveSlot(player, moves, check_pp)
|
|
2987
3075
|
else slot: {
|
|
2988
3076
|
if (showdown) {
|
|
@@ -3068,7 +3156,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3068
3156
|
const stored = side.stored();
|
|
3069
3157
|
|
|
3070
3158
|
// 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
|
|
3159
|
+
// Pokémon Showdown instead produces a list with a single move that must be chosen
|
|
3072
3160
|
//
|
|
3073
3161
|
// Given that no input is allowed on the cartridge 'Pass' seems like it would be logical
|
|
3074
3162
|
// here when not in compatibility mode, but the engine needs to be able to differentiate
|
|
@@ -3076,7 +3164,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3076
3164
|
// passing due to being forced into using a move. Instead of introducing another option
|
|
3077
3165
|
// we simply repurpose Move with no move slot, even though pedantically this is not
|
|
3078
3166
|
// strictly correct as the player would not have been presented the option to move or
|
|
3079
|
-
// switch at all
|
|
3167
|
+
// switch at all
|
|
3080
3168
|
if (isForced(active)) {
|
|
3081
3169
|
out[n] = .{ .type = .Move, .data = @intFromBool(showdown) };
|
|
3082
3170
|
n += 1;
|
|
@@ -3095,7 +3183,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3095
3183
|
// On the cartridge, all of these happen after "FIGHT" (indicating you are not
|
|
3096
3184
|
// switching) but before you are allowed to select a move. Pokémon Showdown instead
|
|
3097
3185
|
// either disables all other moves in the case of limited or requires you to select a
|
|
3098
|
-
// move normally if sleeping/frozen/bound
|
|
3186
|
+
// move normally if sleeping/frozen/bound
|
|
3099
3187
|
if (!showdown and (limited or foe.active.volatiles.Binding or
|
|
3100
3188
|
Status.is(stored.status, .FRZ) or Status.is(stored.status, .SLP)))
|
|
3101
3189
|
{
|
|
@@ -3108,7 +3196,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3108
3196
|
// Pokémon Showdown handles Bide and Binding moves by checking if the move in question
|
|
3109
3197
|
// is present in the Pokémon's moveset (which means moves called via Metronome / Mirror
|
|
3110
3198
|
// 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
|
|
3199
|
+
// proc-ed move in their moveset) and disabling all other moves
|
|
3112
3200
|
if (limited) {
|
|
3113
3201
|
assert(showdown);
|
|
3114
3202
|
assert(side.last_selected_move != .None);
|
|
@@ -3140,7 +3228,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
3140
3228
|
// Struggle (Pokémon Showdown would use 'move 1' here)
|
|
3141
3229
|
if (n == before) {
|
|
3142
3230
|
// GLITCH: Transform + Mirror Move / Metronome PP softlock
|
|
3143
|
-
if (!
|
|
3231
|
+
if (!pkmn.options.mod) {
|
|
3144
3232
|
while (slot <= 4) : (slot += 1) {
|
|
3145
3233
|
if (active.moves[slot - 1].pp != 0) return n;
|
|
3146
3234
|
}
|