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