@pkmn/engine 0.1.0-dev.4eeb346e → 0.1.0-dev.53335a32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -33
- package/build/pkg/data.d.ts +2 -1
- package/build/pkg/data.js +5 -3
- package/build/pkg/data.js.map +1 -1
- package/build/pkg/gen1.js +1 -1
- package/build.zig +27 -17
- package/package.json +23 -33
- package/src/bin/install-pkmn-engine +31 -58
- package/src/lib/binding/node.zig +4 -4
- package/src/lib/binding/wasm.zig +2 -2
- package/src/lib/gen1/README.md +23 -32
- package/src/lib/gen1/data.zig +5 -5
- package/src/lib/gen1/mechanics.zig +237 -250
- package/src/lib/gen2/data.zig +4 -4
- package/src/lib/pkmn.zig +3 -3
- package/build/pkg/addon.d.ts +0 -7
- package/build/pkg/addon.js +0 -111
- package/build/pkg/addon.js.map +0 -1
- package/build/test/display.js +0 -383
- package/build/test/display.js.map +0 -1
- package/build/tools/debug.js +0 -133
- package/build/tools/debug.js.map +0 -1
- package/src/bin/pkmn-debug +0 -62
- package/src/test/display/pkmn.html.tmpl +0 -368
|
@@ -56,8 +56,6 @@ const BOOSTS = &[_][2]u8{
|
|
|
56
56
|
|
|
57
57
|
const MAX_STAT_VALUE = 999;
|
|
58
58
|
|
|
59
|
-
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
60
|
-
|
|
61
59
|
pub fn update(battle: anytype, c1: Choice, c2: Choice, log: anytype) !Result {
|
|
62
60
|
assert(c1.type != .Pass or c2.type != .Pass or battle.turn == 0);
|
|
63
61
|
if (battle.turn == 0) return start(battle, log);
|
|
@@ -187,7 +185,6 @@ fn selectMove(
|
|
|
187
185
|
}
|
|
188
186
|
|
|
189
187
|
// move select
|
|
190
|
-
volatiles.state = 0;
|
|
191
188
|
if (choice.data == 0) {
|
|
192
189
|
const struggle = ok: {
|
|
193
190
|
for (side.active.moves, 0..) |move, i| {
|
|
@@ -304,7 +301,11 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
304
301
|
// Pokémon Showdown's beforeTurnCallback shenanigans
|
|
305
302
|
if (showdown and m1 == .Counter and m2 == .Counter) battle.rng.advance(1);
|
|
306
303
|
|
|
307
|
-
const p1 =
|
|
304
|
+
const p1 = if (showdown)
|
|
305
|
+
battle.rng.range(u8, 0, 2) == 0
|
|
306
|
+
else
|
|
307
|
+
battle.rng.next() < Gen12.percent(50) + 1;
|
|
308
|
+
|
|
308
309
|
if (!showdown) return if (p1) .P1 else .P2;
|
|
309
310
|
|
|
310
311
|
// Pokémon Showdown's "lockedmove" volatile's onBeforeTurn uses BattleQueue#changeAction,
|
|
@@ -314,7 +315,7 @@ fn turnOrder(battle: anytype, c1: Choice, c2: Choice) Player {
|
|
|
314
315
|
const t1 = battle.side(.P1).active.volatiles.Thrashing;
|
|
315
316
|
const t2 = battle.side(.P2).active.volatiles.Thrashing;
|
|
316
317
|
// If *both* sides are thrashing it really should be another speed tie, but we've patched
|
|
317
|
-
// that out and enforce host ordering of events, so P1 just goes first
|
|
318
|
+
// that out and enforce host ordering of events, so P1 just goes first reqgardless of who
|
|
318
319
|
// won the original coin flip
|
|
319
320
|
if (t1 and t2) return .P1;
|
|
320
321
|
return if (p1) if (t1 and !t2) .P2 else .P1 else if (t2 and !t1) .P1 else .P2;
|
|
@@ -449,7 +450,7 @@ fn executeMove(
|
|
|
449
450
|
return doMove(battle, player, mslot, auto, residual, log);
|
|
450
451
|
}
|
|
451
452
|
|
|
452
|
-
const BeforeMove = enum { done, skip_can, skip_pp, ok, err };
|
|
453
|
+
const BeforeMove = union(enum) { done, skip_can, skip_pp, ok, err };
|
|
453
454
|
|
|
454
455
|
fn beforeMove(
|
|
455
456
|
battle: anytype,
|
|
@@ -536,7 +537,12 @@ fn beforeMove(
|
|
|
536
537
|
} else {
|
|
537
538
|
try log.activate(ident, .Confusion);
|
|
538
539
|
|
|
539
|
-
if (
|
|
540
|
+
const confused = if (showdown)
|
|
541
|
+
!battle.rng.chance(u8, 128, 256)
|
|
542
|
+
else
|
|
543
|
+
battle.rng.next() >= Gen12.percent(50) + 1;
|
|
544
|
+
|
|
545
|
+
if (confused) {
|
|
540
546
|
assert(!volatiles.MultiHit);
|
|
541
547
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
542
548
|
volatiles.Bide = false;
|
|
@@ -562,12 +568,8 @@ fn beforeMove(
|
|
|
562
568
|
player
|
|
563
569
|
else
|
|
564
570
|
player.foe();
|
|
565
|
-
|
|
566
|
-
const uncapped = battle.last_damage;
|
|
567
571
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
568
572
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
569
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
570
|
-
if (showdown) battle.last_damage = uncapped;
|
|
571
573
|
|
|
572
574
|
return .done;
|
|
573
575
|
}
|
|
@@ -576,15 +578,22 @@ fn beforeMove(
|
|
|
576
578
|
|
|
577
579
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
578
580
|
|
|
579
|
-
if (Status.is(stored.status, .PAR)
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
581
|
+
if (Status.is(stored.status, .PAR)) {
|
|
582
|
+
const paralyzed = if (showdown)
|
|
583
|
+
battle.rng.chance(u8, 63, 256)
|
|
584
|
+
else
|
|
585
|
+
battle.rng.next() < Gen12.percent(25);
|
|
586
|
+
|
|
587
|
+
if (paralyzed) {
|
|
588
|
+
if (!volatiles.Rage) volatiles.state = 0;
|
|
589
|
+
volatiles.Bide = false;
|
|
590
|
+
volatiles.Thrashing = false;
|
|
591
|
+
volatiles.Charging = false;
|
|
592
|
+
volatiles.Binding = false;
|
|
593
|
+
// GLITCH: Invulnerable is not cleared, resulting in permanent Fly/Dig invulnerability
|
|
594
|
+
try log.cant(ident, .Paralysis);
|
|
595
|
+
return .done;
|
|
596
|
+
}
|
|
588
597
|
}
|
|
589
598
|
|
|
590
599
|
if (volatiles.Bide) {
|
|
@@ -611,9 +620,8 @@ fn beforeMove(
|
|
|
611
620
|
return .done;
|
|
612
621
|
}
|
|
613
622
|
|
|
614
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
615
623
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
616
|
-
if (foe.stored().hp > 0
|
|
624
|
+
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
617
625
|
|
|
618
626
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
619
627
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -779,15 +787,12 @@ fn doMove(
|
|
|
779
787
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
780
788
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
781
789
|
if (immune and move.effect != .Binding) break :miss true;
|
|
782
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
783
|
-
battle.last_damage = 0;
|
|
784
|
-
break :miss true;
|
|
785
|
-
}
|
|
790
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
786
791
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
787
792
|
};
|
|
788
793
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
789
794
|
|
|
790
|
-
var late = showdown
|
|
795
|
+
var late = showdown;
|
|
791
796
|
const skip = status or immune;
|
|
792
797
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
793
798
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -889,9 +894,6 @@ fn doMove(
|
|
|
889
894
|
hits = side.active.volatiles.attacks;
|
|
890
895
|
}
|
|
891
896
|
|
|
892
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
893
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
894
|
-
|
|
895
897
|
var nullified = false;
|
|
896
898
|
var hit: u4 = 0;
|
|
897
899
|
while (hit < hits) {
|
|
@@ -907,7 +909,7 @@ fn doMove(
|
|
|
907
909
|
if (hit == 0 and ohko) try log.ohko();
|
|
908
910
|
hit += 1;
|
|
909
911
|
if (foe.stored().hp == 0) break;
|
|
910
|
-
if (!late
|
|
912
|
+
if (!late) try buildRage(battle, player.foe(), log);
|
|
911
913
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
912
914
|
if (nullified) break;
|
|
913
915
|
}
|
|
@@ -944,8 +946,8 @@ fn doMove(
|
|
|
944
946
|
|
|
945
947
|
if (foe.stored().hp == 0) return null;
|
|
946
948
|
|
|
947
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
948
|
-
if (late and
|
|
949
|
+
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit moves
|
|
950
|
+
if (late and move.effect != .Disable) try buildRage(battle, player.foe(), log);
|
|
949
951
|
|
|
950
952
|
if (!move.effect.isSpecial()) {
|
|
951
953
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -978,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
978
980
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
979
981
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
980
982
|
|
|
981
|
-
return
|
|
983
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
984
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
982
985
|
}
|
|
983
986
|
|
|
984
987
|
fn calcDamage(
|
|
@@ -1082,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1082
1085
|
|
|
1083
1086
|
fn randomizeDamage(battle: anytype) void {
|
|
1084
1087
|
if (battle.last_damage <= 1) return;
|
|
1085
|
-
|
|
1088
|
+
|
|
1089
|
+
const random = if (showdown)
|
|
1090
|
+
battle.rng.range(u8, 217, 256)
|
|
1091
|
+
else loop: {
|
|
1092
|
+
while (true) {
|
|
1093
|
+
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
1094
|
+
if (r >= 217) break :loop r;
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1086
1098
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1087
1099
|
}
|
|
1088
1100
|
|
|
1089
1101
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1090
1102
|
const side = battle.side(player);
|
|
1091
|
-
const foe = battle.foe(player);
|
|
1092
1103
|
|
|
1093
1104
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1094
1105
|
|
|
1095
1106
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1096
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1107
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1097
1108
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1098
1109
|
.SonicBoom => 20,
|
|
1099
1110
|
.DragonRage => 40,
|
|
1100
1111
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1101
1112
|
.Psywave => power: {
|
|
1102
1113
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1114
|
+
if (showdown) {
|
|
1115
|
+
break :power battle.rng.range(u8, 0, max);
|
|
1116
|
+
} else {
|
|
1117
|
+
// GLITCH: Psywave infinite glitch loop
|
|
1118
|
+
if (max <= 1) return Result.Error;
|
|
1119
|
+
while (true) {
|
|
1120
|
+
const r = battle.rng.next();
|
|
1121
|
+
if (r < max) break :power r;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1106
1124
|
},
|
|
1107
1125
|
else => unreachable,
|
|
1108
1126
|
};
|
|
1109
1127
|
|
|
1110
1128
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1111
1129
|
|
|
1112
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1113
1130
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1114
|
-
if (battle.foe(player).stored().hp > 0
|
|
1131
|
+
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1115
1132
|
|
|
1116
1133
|
return null;
|
|
1117
1134
|
}
|
|
@@ -1234,7 +1251,19 @@ fn metronome(
|
|
|
1234
1251
|
) !?Result {
|
|
1235
1252
|
var side = battle.side(player);
|
|
1236
1253
|
|
|
1237
|
-
side.last_selected_move =
|
|
1254
|
+
side.last_selected_move = if (showdown) blk: {
|
|
1255
|
+
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
1256
|
+
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
1257
|
+
break :blk @intToEnum(Move, r + mod);
|
|
1258
|
+
} else loop: {
|
|
1259
|
+
while (true) {
|
|
1260
|
+
const r = battle.rng.next();
|
|
1261
|
+
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
1262
|
+
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
1263
|
+
break :loop @intToEnum(Move, r);
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1238
1267
|
incrementPP(side, mslot);
|
|
1239
1268
|
|
|
1240
1269
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
@@ -1292,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1292
1321
|
}
|
|
1293
1322
|
|
|
1294
1323
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
var accuracy = if (overwritten) state else @as(u16, Gen12.percent(move.accuracy));
|
|
1324
|
+
var state = &side.active.volatiles.state;
|
|
1325
|
+
const overwritten = (move.effect == .Thrashing or move.effect == .Rage) and state.* > 0;
|
|
1326
|
+
assert(!overwritten or (0 < state.* and state.* <= 255 and !side.active.volatiles.Bide));
|
|
1327
|
+
var accuracy = if (overwritten) state.* else @as(u16, Gen12.percent(move.accuracy));
|
|
1301
1328
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1302
1329
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1303
1330
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1304
1331
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1305
1332
|
accuracy = @min(255, @max(1, accuracy));
|
|
1306
1333
|
|
|
1307
|
-
|
|
1308
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1309
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1334
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1310
1335
|
|
|
1311
1336
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1312
|
-
break :miss
|
|
1337
|
+
break :miss if (showdown)
|
|
1338
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
+
else
|
|
1340
|
+
battle.rng.next() >= accuracy;
|
|
1313
1341
|
};
|
|
1314
1342
|
|
|
1315
1343
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1441,15 +1469,14 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1441
1469
|
damage *= volatiles.toxic;
|
|
1442
1470
|
}
|
|
1443
1471
|
|
|
1472
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
+
if (showdown) battle.last_damage = damage;
|
|
1474
|
+
|
|
1444
1475
|
const amount = @min(damage, stored.hp);
|
|
1445
1476
|
stored.hp -= amount;
|
|
1446
1477
|
|
|
1447
1478
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1448
|
-
if (amount > 0)
|
|
1449
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1450
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1451
|
-
if (showdown) battle.last_damage = damage;
|
|
1452
|
-
}
|
|
1479
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1453
1480
|
|
|
1454
1481
|
const before = foe_stored.hp;
|
|
1455
1482
|
// Uncapped damage is added back to the foe
|
|
@@ -1536,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1536
1563
|
return false;
|
|
1537
1564
|
}
|
|
1538
1565
|
|
|
1539
|
-
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1540
|
-
if (side.active.volatiles.disabled_move != 0) {
|
|
1541
|
-
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1542
|
-
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1543
|
-
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1544
|
-
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1545
|
-
m
|
|
1546
|
-
else
|
|
1547
|
-
side.last_selected_move;
|
|
1548
|
-
if (m != .None and m == last) {
|
|
1549
|
-
side.active.volatiles.Charging = false;
|
|
1550
|
-
try log.disabled(ident, last);
|
|
1551
|
-
return true;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
return false;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1558
|
-
const side = battle.side(who);
|
|
1559
|
-
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1560
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1565
|
-
var volatiles = &active.volatiles;
|
|
1566
|
-
assert(volatiles.Thrashing);
|
|
1567
|
-
if (volatiles.attacks > 0) return false;
|
|
1568
|
-
|
|
1569
|
-
volatiles.Thrashing = false;
|
|
1570
|
-
volatiles.Confusion = true;
|
|
1571
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1572
|
-
return true;
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
1566
|
inline fn onBegin(
|
|
1576
1567
|
battle: anytype,
|
|
1577
1568
|
player: Player,
|
|
@@ -1662,7 +1653,10 @@ pub const Effects = struct {
|
|
|
1662
1653
|
side.active.volatiles.Bide = true;
|
|
1663
1654
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1664
1655
|
side.active.volatiles.state = 0;
|
|
1665
|
-
side.active.volatiles.attacks =
|
|
1656
|
+
side.active.volatiles.attacks = @intCast(u3, if (showdown)
|
|
1657
|
+
battle.rng.range(u4, 2, 4)
|
|
1658
|
+
else
|
|
1659
|
+
(battle.rng.next() & 1) + 2);
|
|
1666
1660
|
|
|
1667
1661
|
try log.start(battle.active(player), .Bide);
|
|
1668
1662
|
}
|
|
@@ -1684,8 +1678,14 @@ pub const Effects = struct {
|
|
|
1684
1678
|
return;
|
|
1685
1679
|
}
|
|
1686
1680
|
|
|
1687
|
-
|
|
1688
|
-
|
|
1681
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1682
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .BurnChance1) 26 else 77), 256)
|
|
1683
|
+
else
|
|
1684
|
+
battle.rng.next() < 1 + (if (move.effect == .BurnChance1)
|
|
1685
|
+
Gen12.percent(10)
|
|
1686
|
+
else
|
|
1687
|
+
Gen12.percent(30));
|
|
1688
|
+
if (!chance) return;
|
|
1689
1689
|
|
|
1690
1690
|
foe_stored.status = Status.init(.BRN);
|
|
1691
1691
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1709,7 +1709,11 @@ pub const Effects = struct {
|
|
|
1709
1709
|
const sub = foe.active.volatiles.Substitute;
|
|
1710
1710
|
|
|
1711
1711
|
if (move.effect == .ConfusionChance) {
|
|
1712
|
-
if (
|
|
1712
|
+
const chance = if (showdown)
|
|
1713
|
+
battle.rng.chance(u8, 25, 256)
|
|
1714
|
+
else
|
|
1715
|
+
battle.rng.next() < Gen12.percent(10);
|
|
1716
|
+
if (!chance) return;
|
|
1713
1717
|
} else {
|
|
1714
1718
|
if (showdown) {
|
|
1715
1719
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1728,7 +1732,10 @@ pub const Effects = struct {
|
|
|
1728
1732
|
|
|
1729
1733
|
if (foe.active.volatiles.Confusion) return;
|
|
1730
1734
|
foe.active.volatiles.Confusion = true;
|
|
1731
|
-
foe.active.volatiles.confusion =
|
|
1735
|
+
foe.active.volatiles.confusion = @intCast(u3, if (showdown)
|
|
1736
|
+
battle.rng.range(u8, 2, 6)
|
|
1737
|
+
else
|
|
1738
|
+
(battle.rng.next() & 3) + 2);
|
|
1732
1739
|
|
|
1733
1740
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1734
1741
|
}
|
|
@@ -1778,8 +1785,11 @@ pub const Effects = struct {
|
|
|
1778
1785
|
return Result.Error;
|
|
1779
1786
|
}
|
|
1780
1787
|
|
|
1781
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1782
|
-
volatiles.disabled_duration =
|
|
1788
|
+
volatiles.disabled_move = @intCast(u3, randomMoveSlot(&battle.rng, &foe.active.moves, n));
|
|
1789
|
+
volatiles.disabled_duration = @intCast(u4, if (showdown)
|
|
1790
|
+
battle.rng.range(u8, 1, 9)
|
|
1791
|
+
else
|
|
1792
|
+
(battle.rng.next() & 7) + 1);
|
|
1783
1793
|
|
|
1784
1794
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1785
1795
|
return null;
|
|
@@ -1791,7 +1801,7 @@ pub const Effects = struct {
|
|
|
1791
1801
|
|
|
1792
1802
|
if (battle.last_damage == 0) {
|
|
1793
1803
|
assert(showdown);
|
|
1794
|
-
|
|
1804
|
+
return;
|
|
1795
1805
|
}
|
|
1796
1806
|
|
|
1797
1807
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1818,7 +1828,15 @@ pub const Effects = struct {
|
|
|
1818
1828
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1819
1829
|
|
|
1820
1830
|
if (volatiles.Substitute) return;
|
|
1821
|
-
|
|
1831
|
+
|
|
1832
|
+
const chance = if (showdown)
|
|
1833
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .FlinchChance1) 26 else 77), 256)
|
|
1834
|
+
else
|
|
1835
|
+
battle.rng.next() < 1 + (if (move.effect == .FlinchChance1)
|
|
1836
|
+
Gen12.percent(10)
|
|
1837
|
+
else
|
|
1838
|
+
Gen12.percent(30));
|
|
1839
|
+
if (!chance) return;
|
|
1822
1840
|
|
|
1823
1841
|
volatiles.Flinch = true;
|
|
1824
1842
|
volatiles.Recharging = false;
|
|
@@ -1844,8 +1862,12 @@ pub const Effects = struct {
|
|
|
1844
1862
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1845
1863
|
}
|
|
1846
1864
|
|
|
1847
|
-
|
|
1848
|
-
|
|
1865
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
1866
|
+
battle.rng.chance(u8, 26, 256)
|
|
1867
|
+
else
|
|
1868
|
+
battle.rng.next() < 1 + Gen12.percent(10);
|
|
1869
|
+
if (!chance) return;
|
|
1870
|
+
|
|
1849
1871
|
// Freeze Clause Mod
|
|
1850
1872
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1851
1873
|
|
|
@@ -1990,7 +2012,7 @@ pub const Effects = struct {
|
|
|
1990
2012
|
}
|
|
1991
2013
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1992
2014
|
|
|
1993
|
-
const rslot =
|
|
2015
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
1994
2016
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1995
2017
|
|
|
1996
2018
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2011,8 +2033,7 @@ pub const Effects = struct {
|
|
|
2011
2033
|
assert(!side.active.volatiles.MultiHit);
|
|
2012
2034
|
side.active.volatiles.MultiHit = true;
|
|
2013
2035
|
|
|
2014
|
-
side.active.volatiles.attacks =
|
|
2015
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2036
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2016
2037
|
}
|
|
2017
2038
|
|
|
2018
2039
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2056,8 +2077,14 @@ pub const Effects = struct {
|
|
|
2056
2077
|
}
|
|
2057
2078
|
|
|
2058
2079
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2059
|
-
|
|
2060
|
-
|
|
2080
|
+
const chance = !foe.active.types.includes(move.type) and if (showdown)
|
|
2081
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .ParalyzeChance1) 26 else 77), 256)
|
|
2082
|
+
else
|
|
2083
|
+
battle.rng.next() < 1 + (if (move.effect == .ParalyzeChance1)
|
|
2084
|
+
Gen12.percent(10)
|
|
2085
|
+
else
|
|
2086
|
+
Gen12.percent(30));
|
|
2087
|
+
if (!chance) return;
|
|
2061
2088
|
|
|
2062
2089
|
foe_stored.status = Status.init(.PAR);
|
|
2063
2090
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2100,7 +2127,16 @@ pub const Effects = struct {
|
|
|
2100
2127
|
|
|
2101
2128
|
if (move.effect == .Poison) {
|
|
2102
2129
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2103
|
-
} else
|
|
2130
|
+
} else {
|
|
2131
|
+
const chance = if (showdown)
|
|
2132
|
+
battle.rng.chance(u8, @as(u8, if (move.effect == .PoisonChance1) 52 else 103), 256)
|
|
2133
|
+
else
|
|
2134
|
+
battle.rng.next() < 1 + (if (move.effect == .PoisonChance1)
|
|
2135
|
+
Gen12.percent(20)
|
|
2136
|
+
else
|
|
2137
|
+
Gen12.percent(40));
|
|
2138
|
+
if (!chance) return;
|
|
2139
|
+
}
|
|
2104
2140
|
|
|
2105
2141
|
foe_stored.status = Status.init(.PSN);
|
|
2106
2142
|
if (toxic) {
|
|
@@ -2114,8 +2150,10 @@ pub const Effects = struct {
|
|
|
2114
2150
|
|
|
2115
2151
|
fn rage(battle: anytype, player: Player) !void {
|
|
2116
2152
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2117
|
-
|
|
2153
|
+
if (volatiles.Rage) return;
|
|
2118
2154
|
volatiles.Rage = true;
|
|
2155
|
+
assert(!volatiles.Bide);
|
|
2156
|
+
volatiles.state = 0;
|
|
2119
2157
|
}
|
|
2120
2158
|
|
|
2121
2159
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2167,7 +2205,16 @@ pub const Effects = struct {
|
|
|
2167
2205
|
}
|
|
2168
2206
|
foe.active.volatiles.Recharging = false;
|
|
2169
2207
|
|
|
2170
|
-
|
|
2208
|
+
const duration = @intCast(u3, if (showdown)
|
|
2209
|
+
battle.rng.range(u8, 1, 8)
|
|
2210
|
+
else loop: {
|
|
2211
|
+
while (true) {
|
|
2212
|
+
const r = battle.rng.next() & 7;
|
|
2213
|
+
if (r != 0) break :loop r;
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
foe_stored.status = Status.slp(duration);
|
|
2171
2218
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2172
2219
|
}
|
|
2173
2220
|
|
|
@@ -2220,7 +2267,11 @@ pub const Effects = struct {
|
|
|
2220
2267
|
assert(!volatiles.Bide);
|
|
2221
2268
|
|
|
2222
2269
|
volatiles.Thrashing = true;
|
|
2223
|
-
volatiles.
|
|
2270
|
+
volatiles.state = 0;
|
|
2271
|
+
volatiles.attacks = @intCast(u3, if (showdown)
|
|
2272
|
+
battle.rng.range(u8, 2, 4)
|
|
2273
|
+
else
|
|
2274
|
+
(battle.rng.next() & 1) + 2);
|
|
2224
2275
|
}
|
|
2225
2276
|
|
|
2226
2277
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2262,7 +2313,7 @@ pub const Effects = struct {
|
|
|
2262
2313
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2263
2314
|
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2264
2315
|
|
|
2265
|
-
side.active.volatiles.attacks =
|
|
2316
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2266
2317
|
}
|
|
2267
2318
|
|
|
2268
2319
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2289,11 +2340,9 @@ pub const Effects = struct {
|
|
|
2289
2340
|
return try log.fail(ident, .None);
|
|
2290
2341
|
}
|
|
2291
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2292
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2293
2344
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2294
2345
|
try log.boost(ident, reason, n);
|
|
2295
|
-
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
2296
|
-
if (showdown and move.effect == .Rage) return;
|
|
2297
2346
|
},
|
|
2298
2347
|
.DefenseUp1, .DefenseUp2 => {
|
|
2299
2348
|
assert(boosts.def >= -6 and boosts.def <= 6);
|
|
@@ -2309,7 +2358,7 @@ pub const Effects = struct {
|
|
|
2309
2358
|
return try log.fail(ident, .None);
|
|
2310
2359
|
}
|
|
2311
2360
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2312
|
-
const stat = unmodifiedStats(battle,
|
|
2361
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2313
2362
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2314
2363
|
try log.boost(ident, .Defense, n);
|
|
2315
2364
|
},
|
|
@@ -2326,7 +2375,7 @@ pub const Effects = struct {
|
|
|
2326
2375
|
return try log.fail(ident, .None);
|
|
2327
2376
|
}
|
|
2328
2377
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2329
|
-
const stat = unmodifiedStats(battle,
|
|
2378
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2330
2379
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2331
2380
|
try log.boost(ident, .Speed, 2);
|
|
2332
2381
|
},
|
|
@@ -2346,7 +2395,7 @@ pub const Effects = struct {
|
|
|
2346
2395
|
return try log.fail(ident, .None);
|
|
2347
2396
|
}
|
|
2348
2397
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2349
|
-
const stat = unmodifiedStats(battle,
|
|
2398
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2350
2399
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2351
2400
|
try log.boost(ident, .SpecialAttack, n);
|
|
2352
2401
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2373,7 +2422,11 @@ pub const Effects = struct {
|
|
|
2373
2422
|
}
|
|
2374
2423
|
|
|
2375
2424
|
if (move.effect.isStatDownChance()) {
|
|
2376
|
-
if (
|
|
2425
|
+
const chance = if (showdown)
|
|
2426
|
+
battle.rng.chance(u8, 85, 256)
|
|
2427
|
+
else
|
|
2428
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2429
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2377
2430
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2378
2431
|
return; // checkHit already checks for Invulnerable
|
|
2379
2432
|
}
|
|
@@ -2395,7 +2448,7 @@ pub const Effects = struct {
|
|
|
2395
2448
|
return try log.fail(foe_ident, .None);
|
|
2396
2449
|
}
|
|
2397
2450
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2398
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2451
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2399
2452
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2400
2453
|
try log.boost(foe_ident, .Attack, -1);
|
|
2401
2454
|
},
|
|
@@ -2413,7 +2466,7 @@ pub const Effects = struct {
|
|
|
2413
2466
|
return try log.fail(foe_ident, .None);
|
|
2414
2467
|
}
|
|
2415
2468
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2416
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2469
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2417
2470
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2418
2471
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2419
2472
|
},
|
|
@@ -2430,7 +2483,7 @@ pub const Effects = struct {
|
|
|
2430
2483
|
return try log.fail(foe_ident, .None);
|
|
2431
2484
|
}
|
|
2432
2485
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2433
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2486
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2434
2487
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2435
2488
|
try log.boost(foe_ident, .Speed, -1);
|
|
2436
2489
|
assert(boosts.spe >= -6);
|
|
@@ -2450,7 +2503,7 @@ pub const Effects = struct {
|
|
|
2450
2503
|
return try log.fail(foe_ident, .None);
|
|
2451
2504
|
}
|
|
2452
2505
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2453
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2506
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2454
2507
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2455
2508
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2456
2509
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2469,7 +2522,8 @@ pub const Effects = struct {
|
|
|
2469
2522
|
}
|
|
2470
2523
|
};
|
|
2471
2524
|
|
|
2472
|
-
fn unmodifiedStats(battle: anytype,
|
|
2525
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2526
|
+
const side = battle.side(who);
|
|
2473
2527
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2474
2528
|
const id = ID.from(side.active.volatiles.transform);
|
|
2475
2529
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2535,140 +2589,75 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2535
2589
|
}
|
|
2536
2590
|
}
|
|
2537
2591
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
inline fn damage(battle: anytype) u8 {
|
|
2550
|
-
if (showdown) return battle.rng.range(u8, 217, 256);
|
|
2551
|
-
while (true) {
|
|
2552
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2553
|
-
if (r >= 217) return r;
|
|
2592
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2593
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2594
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2595
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2596
|
+
if (m != .None and m == side.last_selected_move) {
|
|
2597
|
+
side.active.volatiles.Charging = false;
|
|
2598
|
+
try log.disabled(ident, side.last_selected_move);
|
|
2599
|
+
return true;
|
|
2554
2600
|
}
|
|
2555
2601
|
}
|
|
2602
|
+
return false;
|
|
2603
|
+
}
|
|
2556
2604
|
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
inline fn confusionDuration(battle: anytype) u3 {
|
|
2563
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2564
|
-
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
inline fn confused(battle: anytype) bool {
|
|
2568
|
-
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2569
|
-
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
inline fn paralyzed(battle: anytype) bool {
|
|
2573
|
-
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2574
|
-
return battle.rng.next() < Gen12.percent(25);
|
|
2605
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
2606
|
+
const side = battle.side(who);
|
|
2607
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
2608
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2575
2609
|
}
|
|
2610
|
+
}
|
|
2576
2611
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2612
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2613
|
+
var volatiles = &active.volatiles;
|
|
2614
|
+
assert(volatiles.Thrashing);
|
|
2615
|
+
if (volatiles.attacks > 0) return false;
|
|
2581
2616
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2617
|
+
volatiles.Thrashing = false;
|
|
2618
|
+
volatiles.Confusion = true;
|
|
2619
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2620
|
+
battle.rng.range(u8, 2, 6)
|
|
2621
|
+
else
|
|
2622
|
+
(battle.rng.next() & 3) + 2);
|
|
2586
2623
|
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2590
|
-
}
|
|
2624
|
+
return true;
|
|
2625
|
+
}
|
|
2591
2626
|
|
|
2592
|
-
|
|
2593
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2594
|
-
while (true) {
|
|
2595
|
-
const r = battle.rng.next() & 7;
|
|
2596
|
-
if (r != 0) return @intCast(u3, r);
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2627
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2599
2628
|
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2629
|
+
fn distribution(battle: anytype) u3 {
|
|
2630
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2631
|
+
const r = (battle.rng.next() & 3);
|
|
2632
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2633
|
+
}
|
|
2604
2634
|
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
if (
|
|
2620
|
-
r -= 1;
|
|
2621
|
-
if (r == 0) break;
|
|
2622
|
-
}
|
|
2635
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2636
|
+
if (showdown) {
|
|
2637
|
+
if (check_pp == 0) {
|
|
2638
|
+
var i: usize = moves.len;
|
|
2639
|
+
while (i > 0) {
|
|
2640
|
+
i -= 1;
|
|
2641
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2642
|
+
}
|
|
2643
|
+
} else {
|
|
2644
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2645
|
+
var i: usize = 0;
|
|
2646
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2647
|
+
if (moves[i].pp > 0) {
|
|
2648
|
+
r -= 1;
|
|
2649
|
+
if (r == 0) break;
|
|
2623
2650
|
}
|
|
2624
|
-
return @intCast(u4, i + 1);
|
|
2625
2651
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
while (true) {
|
|
2629
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2630
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2652
|
+
return @intCast(u4, i + 1);
|
|
2631
2653
|
}
|
|
2632
2654
|
}
|
|
2633
2655
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2656
|
+
while (true) {
|
|
2657
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2658
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2637
2659
|
}
|
|
2638
|
-
|
|
2639
|
-
inline fn metronome(battle: anytype) Move {
|
|
2640
|
-
if (showdown) {
|
|
2641
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2642
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2643
|
-
return @intToEnum(Move, r + mod);
|
|
2644
|
-
}
|
|
2645
|
-
while (true) {
|
|
2646
|
-
const r = battle.rng.next();
|
|
2647
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2648
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2649
|
-
return @intToEnum(Move, r);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
inline fn psywave(battle: anytype, max: u8) u8 {
|
|
2654
|
-
if (showdown) return battle.rng.range(u8, 0, max);
|
|
2655
|
-
while (true) {
|
|
2656
|
-
const r = battle.rng.next();
|
|
2657
|
-
if (r < max) return r;
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2662
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2663
|
-
const r = (battle.rng.next() & 3);
|
|
2664
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
inline fn unboost(battle: anytype) bool {
|
|
2668
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2669
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2670
|
-
}
|
|
2671
|
-
};
|
|
2660
|
+
}
|
|
2672
2661
|
|
|
2673
2662
|
test "RNG agreement" {
|
|
2674
2663
|
if (!showdown) return;
|
|
@@ -2768,11 +2757,9 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2768
2757
|
while (slot <= 4) : (slot += 1) {
|
|
2769
2758
|
const m = active.moves[slot - 1];
|
|
2770
2759
|
if (m.id == .None) break;
|
|
2771
|
-
if (m.id ==
|
|
2772
|
-
// Pokémon Showdown displays Struggle if limited to Bide
|
|
2773
|
-
const
|
|
2774
|
-
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|
|
2775
|
-
const s = if (struggle) 0 else slot;
|
|
2760
|
+
if (m.id == side.last_selected_move) {
|
|
2761
|
+
// Pokémon Showdown displays Struggle if limited to Bide when out of PP
|
|
2762
|
+
const s = if (m.pp == 0 and m.id == .Bide) 0 else slot;
|
|
2776
2763
|
out[n] = .{ .type = .Move, .data = s };
|
|
2777
2764
|
n += 1;
|
|
2778
2765
|
return n;
|