@pkmn/engine 0.1.0-dev.a5a9f4b2 → 0.1.0-dev.b9fb6eab
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 +9 -37
- 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 +15 -10
- package/package.json +28 -38
- package/src/bin/install-pkmn-engine +1 -1
- package/src/lib/binding/node.zig +4 -4
- package/src/lib/binding/wasm.zig +2 -2
- package/src/lib/gen1/README.md +5 -16
- package/src/lib/gen1/data.zig +5 -5
- package/src/lib/gen1/mechanics.zig +242 -247
- 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,
|
|
@@ -512,10 +513,9 @@ fn beforeMove(
|
|
|
512
513
|
// Pokémon Showdown's disable condition has a single onBeforeMove handler
|
|
513
514
|
if (showdown and try disabled(side, ident, log)) return .done;
|
|
514
515
|
|
|
515
|
-
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
516
|
-
// until its imaginary "residual" phase, meaning Pokémon can sometimes flinch multiple times
|
|
517
|
-
// from the same original hit
|
|
516
|
+
// Pokémon Showdown checks for Flinch *after* instead of before
|
|
518
517
|
if (showdown and volatiles.Flinch) {
|
|
518
|
+
volatiles.Flinch = false;
|
|
519
519
|
try log.cant(ident, .Flinch);
|
|
520
520
|
return .done;
|
|
521
521
|
}
|
|
@@ -537,7 +537,12 @@ fn beforeMove(
|
|
|
537
537
|
} else {
|
|
538
538
|
try log.activate(ident, .Confusion);
|
|
539
539
|
|
|
540
|
-
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) {
|
|
541
546
|
assert(!volatiles.MultiHit);
|
|
542
547
|
if (!volatiles.Rage) volatiles.state = 0;
|
|
543
548
|
volatiles.Bide = false;
|
|
@@ -563,12 +568,8 @@ fn beforeMove(
|
|
|
563
568
|
player
|
|
564
569
|
else
|
|
565
570
|
player.foe();
|
|
566
|
-
|
|
567
|
-
const uncapped = battle.last_damage;
|
|
568
571
|
// Skipping adjustDamage / randomizeDamage / checkHit
|
|
569
572
|
_ = try applyDamage(battle, player, target, .Confusion, log);
|
|
570
|
-
// Pokémon Showdown thinks that confusion damage is uncapped ¯\_(ツ)_/¯
|
|
571
|
-
if (showdown) battle.last_damage = uncapped;
|
|
572
573
|
|
|
573
574
|
return .done;
|
|
574
575
|
}
|
|
@@ -577,15 +578,22 @@ fn beforeMove(
|
|
|
577
578
|
|
|
578
579
|
if (!showdown and try disabled(side, ident, log)) return .done;
|
|
579
580
|
|
|
580
|
-
if (Status.is(stored.status, .PAR)
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
}
|
|
589
597
|
}
|
|
590
598
|
|
|
591
599
|
if (volatiles.Bide) {
|
|
@@ -612,9 +620,8 @@ fn beforeMove(
|
|
|
612
620
|
return .done;
|
|
613
621
|
}
|
|
614
622
|
|
|
615
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
616
623
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
617
|
-
if (foe.stored().hp > 0
|
|
624
|
+
if (foe.stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
618
625
|
|
|
619
626
|
// For reasons passing understanding, Pokémon Showdown still inflicts residual damage to
|
|
620
627
|
// Bide's user even if the above damage has caused the foe to faint. It's simpler to always
|
|
@@ -780,15 +787,12 @@ fn doMove(
|
|
|
780
787
|
(@enumToInt(move.type.effectiveness(foe.active.types.type1)) == 0 or
|
|
781
788
|
@enumToInt(move.type.effectiveness(foe.active.types.type2)) == 0);
|
|
782
789
|
if (immune and move.effect != .Binding) break :miss true;
|
|
783
|
-
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe)
|
|
784
|
-
battle.last_damage = 0;
|
|
785
|
-
break :miss true;
|
|
786
|
-
}
|
|
790
|
+
if (move.effect == .OHKO and side.active.stats.spe < foe.active.stats.spe) break :miss true;
|
|
787
791
|
break :miss move.target != .Self and !moveHit(battle, player, move, &immune, &mist);
|
|
788
792
|
};
|
|
789
793
|
assert(!immune or miss or (showdown and move.effect == .Binding));
|
|
790
794
|
|
|
791
|
-
var late = showdown
|
|
795
|
+
var late = showdown;
|
|
792
796
|
const skip = status or immune;
|
|
793
797
|
if ((!showdown or (!skip or counter)) and !miss) blk: {
|
|
794
798
|
if (showdown and move.effect.isMulti()) {
|
|
@@ -890,9 +894,6 @@ fn doMove(
|
|
|
890
894
|
hits = side.active.volatiles.attacks;
|
|
891
895
|
}
|
|
892
896
|
|
|
893
|
-
// Pokémon Showdown only builds Rage for Disable/Explosion (hit/miss) when attacking into a sub
|
|
894
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
895
|
-
|
|
896
897
|
var nullified = false;
|
|
897
898
|
var hit: u4 = 0;
|
|
898
899
|
while (hit < hits) {
|
|
@@ -908,7 +909,7 @@ fn doMove(
|
|
|
908
909
|
if (hit == 0 and ohko) try log.ohko();
|
|
909
910
|
hit += 1;
|
|
910
911
|
if (foe.stored().hp == 0) break;
|
|
911
|
-
if (!late
|
|
912
|
+
if (!late) try buildRage(battle, player.foe(), log);
|
|
912
913
|
// If the substitute breaks during a multi-hit attack, the attack ends
|
|
913
914
|
if (nullified) break;
|
|
914
915
|
}
|
|
@@ -945,8 +946,8 @@ fn doMove(
|
|
|
945
946
|
|
|
946
947
|
if (foe.stored().hp == 0) return null;
|
|
947
948
|
|
|
948
|
-
// Pokémon Showdown builds Rage at the wrong time for non-MultiHit
|
|
949
|
-
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);
|
|
950
951
|
|
|
951
952
|
if (!move.effect.isSpecial()) {
|
|
952
953
|
// On the cartridge Rage is not considered to be "special" and thus gets executed for a
|
|
@@ -979,7 +980,8 @@ fn checkCriticalHit(battle: anytype, player: Player, move: Move.Data) bool {
|
|
|
979
980
|
chance = if (side.active.volatiles.FocusEnergy) chance / 2 else @min(chance * 2, 255);
|
|
980
981
|
chance = if (move.effect == .HighCritical) @min(chance * 4, 255) else chance / 2;
|
|
981
982
|
|
|
982
|
-
return
|
|
983
|
+
if (showdown) return battle.rng.chance(u8, @intCast(u8, chance), 256);
|
|
984
|
+
return std.math.rotl(u8, battle.rng.next(), 3) < chance;
|
|
983
985
|
}
|
|
984
986
|
|
|
985
987
|
fn calcDamage(
|
|
@@ -1083,36 +1085,50 @@ fn adjustDamage(battle: anytype, player: Player) u16 {
|
|
|
1083
1085
|
|
|
1084
1086
|
fn randomizeDamage(battle: anytype) void {
|
|
1085
1087
|
if (battle.last_damage <= 1) return;
|
|
1086
|
-
|
|
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
|
+
|
|
1087
1098
|
battle.last_damage = @intCast(u16, @as(u32, battle.last_damage) *% random / 255);
|
|
1088
1099
|
}
|
|
1089
1100
|
|
|
1090
1101
|
fn specialDamage(battle: anytype, player: Player, move: Move.Data, log: anytype) !?Result {
|
|
1091
1102
|
const side = battle.side(player);
|
|
1092
|
-
const foe = battle.foe(player);
|
|
1093
1103
|
|
|
1094
1104
|
if (!try checkHit(battle, player, move, log)) return null;
|
|
1095
1105
|
|
|
1096
1106
|
battle.last_damage = switch (side.last_selected_move) {
|
|
1097
|
-
.SuperFang => @max(foe.stored().hp / 2, 1),
|
|
1107
|
+
.SuperFang => @max(battle.foe(player).stored().hp / 2, 1),
|
|
1098
1108
|
.SeismicToss, .NightShade => side.stored().level,
|
|
1099
1109
|
.SonicBoom => 20,
|
|
1100
1110
|
.DragonRage => 40,
|
|
1101
1111
|
// GLITCH: if power = 0 then a desync occurs (or a miss on Pokémon Showdown)
|
|
1102
1112
|
.Psywave => power: {
|
|
1103
1113
|
const max = @intCast(u8, @as(u16, side.stored().level) * 3 / 2);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
+
}
|
|
1107
1124
|
},
|
|
1108
1125
|
else => unreachable,
|
|
1109
1126
|
};
|
|
1110
1127
|
|
|
1111
1128
|
if (battle.last_damage == 0) return if (showdown) null else Result.Error;
|
|
1112
1129
|
|
|
1113
|
-
const sub = showdown and foe.active.volatiles.Substitute;
|
|
1114
1130
|
_ = try applyDamage(battle, player.foe(), player.foe(), .None, log);
|
|
1115
|
-
if (battle.foe(player).stored().hp > 0
|
|
1131
|
+
if (battle.foe(player).stored().hp > 0) try buildRage(battle, player.foe(), log);
|
|
1116
1132
|
|
|
1117
1133
|
return null;
|
|
1118
1134
|
}
|
|
@@ -1235,7 +1251,19 @@ fn metronome(
|
|
|
1235
1251
|
) !?Result {
|
|
1236
1252
|
var side = battle.side(player);
|
|
1237
1253
|
|
|
1238
|
-
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
|
+
|
|
1239
1267
|
incrementPP(side, mslot);
|
|
1240
1268
|
|
|
1241
1269
|
if (!try canMove(battle, player, mslot, auto, false, .Metronome, residual, log)) return null;
|
|
@@ -1293,24 +1321,23 @@ fn moveHit(battle: anytype, player: Player, move: Move.Data, immune: *bool, mist
|
|
|
1293
1321
|
}
|
|
1294
1322
|
|
|
1295
1323
|
// GLITCH: Thrash / Petal Dance / Rage get their accuracy overwritten on subsequent hits
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
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));
|
|
1302
1328
|
var boost = BOOSTS[@intCast(u4, @as(i8, side.active.boosts.accuracy) + 6)];
|
|
1303
1329
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1304
1330
|
boost = BOOSTS[@intCast(u4, @as(i8, -foe.active.boosts.evasion) + 6)];
|
|
1305
1331
|
accuracy = accuracy * boost[0] / boost[1];
|
|
1306
1332
|
accuracy = @min(255, @max(1, accuracy));
|
|
1307
1333
|
|
|
1308
|
-
|
|
1309
|
-
if (showdown) overwrite = side.active.volatiles.Rage or side.active.volatiles.Thrashing;
|
|
1310
|
-
if (overwrite) side.active.volatiles.state = accuracy;
|
|
1334
|
+
if (move.effect == .Thrashing or move.effect == .Rage) state.* = accuracy;
|
|
1311
1335
|
|
|
1312
1336
|
// GLITCH: max accuracy is 255 so 1/256 chance of miss
|
|
1313
|
-
break :miss
|
|
1337
|
+
break :miss if (showdown)
|
|
1338
|
+
!battle.rng.chance(u8, @intCast(u8, accuracy), 256)
|
|
1339
|
+
else
|
|
1340
|
+
battle.rng.next() >= accuracy;
|
|
1314
1341
|
};
|
|
1315
1342
|
|
|
1316
1343
|
// Pokémon Showdown reports miss instead of fail for moves blocked by Mist that 1/256 miss
|
|
@@ -1442,15 +1469,14 @@ fn handleResidual(battle: anytype, player: Player, log: anytype) !void {
|
|
|
1442
1469
|
damage *= volatiles.toxic;
|
|
1443
1470
|
}
|
|
1444
1471
|
|
|
1472
|
+
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1473
|
+
if (showdown) battle.last_damage = damage;
|
|
1474
|
+
|
|
1445
1475
|
const amount = @min(damage, stored.hp);
|
|
1446
1476
|
stored.hp -= amount;
|
|
1447
1477
|
|
|
1448
1478
|
// As above, Pokémon Showdown uses damageOf but its not relevant
|
|
1449
|
-
if (amount > 0)
|
|
1450
|
-
try log.damage(ident, stored, .LeechSeed);
|
|
1451
|
-
// Pokémon Showdown erroneously updates last damage with uncapped Leech Seed damage
|
|
1452
|
-
if (showdown) battle.last_damage = damage;
|
|
1453
|
-
}
|
|
1479
|
+
if (amount > 0) try log.damage(ident, stored, .LeechSeed);
|
|
1454
1480
|
|
|
1455
1481
|
const before = foe_stored.hp;
|
|
1456
1482
|
// Uncapped damage is added back to the foe
|
|
@@ -1537,42 +1563,6 @@ fn checkEBC(battle: anytype) bool {
|
|
|
1537
1563
|
return false;
|
|
1538
1564
|
}
|
|
1539
1565
|
|
|
1540
|
-
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
1541
|
-
if (side.active.volatiles.disabled_move != 0) {
|
|
1542
|
-
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
1543
|
-
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
1544
|
-
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
1545
|
-
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
1546
|
-
m
|
|
1547
|
-
else
|
|
1548
|
-
side.last_selected_move;
|
|
1549
|
-
if (m != .None and m == last) {
|
|
1550
|
-
side.active.volatiles.Charging = false;
|
|
1551
|
-
try log.disabled(ident, last);
|
|
1552
|
-
return true;
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
return false;
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
1559
|
-
const side = battle.side(who);
|
|
1560
|
-
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
1561
|
-
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
1566
|
-
var volatiles = &active.volatiles;
|
|
1567
|
-
assert(volatiles.Thrashing);
|
|
1568
|
-
if (volatiles.attacks > 0) return false;
|
|
1569
|
-
|
|
1570
|
-
volatiles.Thrashing = false;
|
|
1571
|
-
volatiles.Confusion = true;
|
|
1572
|
-
volatiles.confusion = Rolls.confusionDuration(battle);
|
|
1573
|
-
return true;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
1566
|
inline fn onBegin(
|
|
1577
1567
|
battle: anytype,
|
|
1578
1568
|
player: Player,
|
|
@@ -1663,7 +1653,10 @@ pub const Effects = struct {
|
|
|
1663
1653
|
side.active.volatiles.Bide = true;
|
|
1664
1654
|
assert(!side.active.volatiles.Thrashing and !side.active.volatiles.Rage);
|
|
1665
1655
|
side.active.volatiles.state = 0;
|
|
1666
|
-
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);
|
|
1667
1660
|
|
|
1668
1661
|
try log.start(battle.active(player), .Bide);
|
|
1669
1662
|
}
|
|
@@ -1685,8 +1678,14 @@ pub const Effects = struct {
|
|
|
1685
1678
|
return;
|
|
1686
1679
|
}
|
|
1687
1680
|
|
|
1688
|
-
|
|
1689
|
-
|
|
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;
|
|
1690
1689
|
|
|
1691
1690
|
foe_stored.status = Status.init(.BRN);
|
|
1692
1691
|
foe.active.stats.atk = @max(foe.active.stats.atk / 2, 1);
|
|
@@ -1710,7 +1709,11 @@ pub const Effects = struct {
|
|
|
1710
1709
|
const sub = foe.active.volatiles.Substitute;
|
|
1711
1710
|
|
|
1712
1711
|
if (move.effect == .ConfusionChance) {
|
|
1713
|
-
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;
|
|
1714
1717
|
} else {
|
|
1715
1718
|
if (showdown) {
|
|
1716
1719
|
if (!try checkHit(battle, player, move, log)) {
|
|
@@ -1729,7 +1732,10 @@ pub const Effects = struct {
|
|
|
1729
1732
|
|
|
1730
1733
|
if (foe.active.volatiles.Confusion) return;
|
|
1731
1734
|
foe.active.volatiles.Confusion = true;
|
|
1732
|
-
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);
|
|
1733
1739
|
|
|
1734
1740
|
try log.start(battle.active(player.foe()), .Confusion);
|
|
1735
1741
|
}
|
|
@@ -1779,8 +1785,11 @@ pub const Effects = struct {
|
|
|
1779
1785
|
return Result.Error;
|
|
1780
1786
|
}
|
|
1781
1787
|
|
|
1782
|
-
volatiles.disabled_move = @intCast(u3,
|
|
1783
|
-
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);
|
|
1784
1793
|
|
|
1785
1794
|
try log.startEffect(foe_ident, .Disable, foe.active.move(volatiles.disabled_move).id);
|
|
1786
1795
|
return null;
|
|
@@ -1792,7 +1801,7 @@ pub const Effects = struct {
|
|
|
1792
1801
|
|
|
1793
1802
|
if (battle.last_damage == 0) {
|
|
1794
1803
|
assert(showdown);
|
|
1795
|
-
|
|
1804
|
+
return;
|
|
1796
1805
|
}
|
|
1797
1806
|
|
|
1798
1807
|
const drain = @max(battle.last_damage / 2, 1);
|
|
@@ -1819,7 +1828,15 @@ pub const Effects = struct {
|
|
|
1819
1828
|
var volatiles = &battle.foe(player).active.volatiles;
|
|
1820
1829
|
|
|
1821
1830
|
if (volatiles.Substitute) return;
|
|
1822
|
-
|
|
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;
|
|
1823
1840
|
|
|
1824
1841
|
volatiles.Flinch = true;
|
|
1825
1842
|
volatiles.Recharging = false;
|
|
@@ -1845,8 +1862,12 @@ pub const Effects = struct {
|
|
|
1845
1862
|
return if (showdown and !foe.active.types.includes(move.type)) battle.rng.advance(1);
|
|
1846
1863
|
}
|
|
1847
1864
|
|
|
1848
|
-
|
|
1849
|
-
|
|
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
|
+
|
|
1850
1871
|
// Freeze Clause Mod
|
|
1851
1872
|
if (showdown) for (foe.pokemon) |p| if (Status.is(p.status, .FRZ)) return;
|
|
1852
1873
|
|
|
@@ -1991,7 +2012,7 @@ pub const Effects = struct {
|
|
|
1991
2012
|
}
|
|
1992
2013
|
if (!try checkHit(battle, player, move, log)) return;
|
|
1993
2014
|
|
|
1994
|
-
const rslot =
|
|
2015
|
+
const rslot = randomMoveSlot(&battle.rng, &foe.active.moves, 0);
|
|
1995
2016
|
side.active.move(oslot).id = foe.active.move(rslot).id;
|
|
1996
2017
|
|
|
1997
2018
|
try log.startEffect(battle.active(player), .Mimic, side.active.move(oslot).id);
|
|
@@ -2012,8 +2033,7 @@ pub const Effects = struct {
|
|
|
2012
2033
|
assert(!side.active.volatiles.MultiHit);
|
|
2013
2034
|
side.active.volatiles.MultiHit = true;
|
|
2014
2035
|
|
|
2015
|
-
side.active.volatiles.attacks =
|
|
2016
|
-
if (move.effect == .MultiHit) Rolls.distribution(battle) else 2;
|
|
2036
|
+
side.active.volatiles.attacks = if (move.effect == .MultiHit) distribution(battle) else 2;
|
|
2017
2037
|
}
|
|
2018
2038
|
|
|
2019
2039
|
fn paralyze(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2057,8 +2077,14 @@ pub const Effects = struct {
|
|
|
2057
2077
|
}
|
|
2058
2078
|
|
|
2059
2079
|
// Body Slam can't paralyze a Normal type Pokémon
|
|
2060
|
-
|
|
2061
|
-
|
|
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;
|
|
2062
2088
|
|
|
2063
2089
|
foe_stored.status = Status.init(.PAR);
|
|
2064
2090
|
foe.active.stats.spe = @max(foe.active.stats.spe / 4, 1);
|
|
@@ -2101,7 +2127,16 @@ pub const Effects = struct {
|
|
|
2101
2127
|
|
|
2102
2128
|
if (move.effect == .Poison) {
|
|
2103
2129
|
if (!showdown and !try checkHit(battle, player, move, log)) return;
|
|
2104
|
-
} 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
|
+
}
|
|
2105
2140
|
|
|
2106
2141
|
foe_stored.status = Status.init(.PSN);
|
|
2107
2142
|
if (toxic) {
|
|
@@ -2115,8 +2150,10 @@ pub const Effects = struct {
|
|
|
2115
2150
|
|
|
2116
2151
|
fn rage(battle: anytype, player: Player) !void {
|
|
2117
2152
|
var volatiles = &battle.side(player).active.volatiles;
|
|
2118
|
-
|
|
2153
|
+
if (volatiles.Rage) return;
|
|
2119
2154
|
volatiles.Rage = true;
|
|
2155
|
+
assert(!volatiles.Bide);
|
|
2156
|
+
volatiles.state = 0;
|
|
2120
2157
|
}
|
|
2121
2158
|
|
|
2122
2159
|
fn recoil(battle: anytype, player: Player, residual: *bool, log: anytype) !void {
|
|
@@ -2168,7 +2205,16 @@ pub const Effects = struct {
|
|
|
2168
2205
|
}
|
|
2169
2206
|
foe.active.volatiles.Recharging = false;
|
|
2170
2207
|
|
|
2171
|
-
|
|
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);
|
|
2172
2218
|
try log.statusFrom(foe_ident, foe_stored.status, battle.side(player).last_selected_move);
|
|
2173
2219
|
}
|
|
2174
2220
|
|
|
@@ -2221,7 +2267,11 @@ pub const Effects = struct {
|
|
|
2221
2267
|
assert(!volatiles.Bide);
|
|
2222
2268
|
|
|
2223
2269
|
volatiles.Thrashing = true;
|
|
2224
|
-
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);
|
|
2225
2275
|
}
|
|
2226
2276
|
|
|
2227
2277
|
fn transform(battle: anytype, player: Player, log: anytype) !void {
|
|
@@ -2263,7 +2313,7 @@ pub const Effects = struct {
|
|
|
2263
2313
|
// (Pokémon Showdown unitentionally patches this glitch, preventing automatic selection)
|
|
2264
2314
|
if (!showdown) foe.active.volatiles.Recharging = false;
|
|
2265
2315
|
|
|
2266
|
-
side.active.volatiles.attacks =
|
|
2316
|
+
side.active.volatiles.attacks = distribution(battle) - 1;
|
|
2267
2317
|
}
|
|
2268
2318
|
|
|
2269
2319
|
fn boost(battle: anytype, player: Player, move: Move.Data, log: anytype) !void {
|
|
@@ -2290,7 +2340,7 @@ pub const Effects = struct {
|
|
|
2290
2340
|
return try log.fail(ident, .None);
|
|
2291
2341
|
}
|
|
2292
2342
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2293
|
-
const stat = unmodifiedStats(battle,
|
|
2343
|
+
const stat = unmodifiedStats(battle, player).atk;
|
|
2294
2344
|
stats.atk = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2295
2345
|
try log.boost(ident, reason, n);
|
|
2296
2346
|
// Pokémon Showdown doesn't re-apply status modifiers after Rage boosts
|
|
@@ -2310,7 +2360,7 @@ pub const Effects = struct {
|
|
|
2310
2360
|
return try log.fail(ident, .None);
|
|
2311
2361
|
}
|
|
2312
2362
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2313
|
-
const stat = unmodifiedStats(battle,
|
|
2363
|
+
const stat = unmodifiedStats(battle, player).def;
|
|
2314
2364
|
stats.def = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2315
2365
|
try log.boost(ident, .Defense, n);
|
|
2316
2366
|
},
|
|
@@ -2327,7 +2377,7 @@ pub const Effects = struct {
|
|
|
2327
2377
|
return try log.fail(ident, .None);
|
|
2328
2378
|
}
|
|
2329
2379
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2330
|
-
const stat = unmodifiedStats(battle,
|
|
2380
|
+
const stat = unmodifiedStats(battle, player).spe;
|
|
2331
2381
|
stats.spe = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2332
2382
|
try log.boost(ident, .Speed, 2);
|
|
2333
2383
|
},
|
|
@@ -2347,7 +2397,7 @@ pub const Effects = struct {
|
|
|
2347
2397
|
return try log.fail(ident, .None);
|
|
2348
2398
|
}
|
|
2349
2399
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2350
|
-
const stat = unmodifiedStats(battle,
|
|
2400
|
+
const stat = unmodifiedStats(battle, player).spc;
|
|
2351
2401
|
stats.spc = @min(MAX_STAT_VALUE, stat * mod[0] / mod[1]);
|
|
2352
2402
|
try log.boost(ident, .SpecialAttack, n);
|
|
2353
2403
|
try log.boost(ident, .SpecialDefense, n);
|
|
@@ -2374,7 +2424,11 @@ pub const Effects = struct {
|
|
|
2374
2424
|
}
|
|
2375
2425
|
|
|
2376
2426
|
if (move.effect.isStatDownChance()) {
|
|
2377
|
-
if (
|
|
2427
|
+
const chance = if (showdown)
|
|
2428
|
+
battle.rng.chance(u8, 85, 256)
|
|
2429
|
+
else
|
|
2430
|
+
battle.rng.next() < Gen12.percent(33) + 1;
|
|
2431
|
+
if (!chance or foe.active.volatiles.Invulnerable) return;
|
|
2378
2432
|
} else if (!showdown and !try checkHit(battle, player, move, log)) {
|
|
2379
2433
|
return; // checkHit already checks for Invulnerable
|
|
2380
2434
|
}
|
|
@@ -2396,7 +2450,7 @@ pub const Effects = struct {
|
|
|
2396
2450
|
return try log.fail(foe_ident, .None);
|
|
2397
2451
|
}
|
|
2398
2452
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.atk) + 6)];
|
|
2399
|
-
const stat = unmodifiedStats(battle, foe).atk;
|
|
2453
|
+
const stat = unmodifiedStats(battle, player.foe()).atk;
|
|
2400
2454
|
stats.atk = @max(1, stat * mod[0] / mod[1]);
|
|
2401
2455
|
try log.boost(foe_ident, .Attack, -1);
|
|
2402
2456
|
},
|
|
@@ -2414,7 +2468,7 @@ pub const Effects = struct {
|
|
|
2414
2468
|
return try log.fail(foe_ident, .None);
|
|
2415
2469
|
}
|
|
2416
2470
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.def) + 6)];
|
|
2417
|
-
const stat = unmodifiedStats(battle, foe).def;
|
|
2471
|
+
const stat = unmodifiedStats(battle, player.foe()).def;
|
|
2418
2472
|
stats.def = @max(1, stat * mod[0] / mod[1]);
|
|
2419
2473
|
try log.boost(foe_ident, .Defense, -@as(i8, n));
|
|
2420
2474
|
},
|
|
@@ -2431,7 +2485,7 @@ pub const Effects = struct {
|
|
|
2431
2485
|
return try log.fail(foe_ident, .None);
|
|
2432
2486
|
}
|
|
2433
2487
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spe) + 6)];
|
|
2434
|
-
const stat = unmodifiedStats(battle, foe).spe;
|
|
2488
|
+
const stat = unmodifiedStats(battle, player.foe()).spe;
|
|
2435
2489
|
stats.spe = @max(1, stat * mod[0] / mod[1]);
|
|
2436
2490
|
try log.boost(foe_ident, .Speed, -1);
|
|
2437
2491
|
assert(boosts.spe >= -6);
|
|
@@ -2451,7 +2505,7 @@ pub const Effects = struct {
|
|
|
2451
2505
|
return try log.fail(foe_ident, .None);
|
|
2452
2506
|
}
|
|
2453
2507
|
var mod = BOOSTS[@intCast(u4, @as(i8, boosts.spc) + 6)];
|
|
2454
|
-
const stat = unmodifiedStats(battle, foe).spc;
|
|
2508
|
+
const stat = unmodifiedStats(battle, player.foe()).spc;
|
|
2455
2509
|
stats.spc = @max(1, stat * mod[0] / mod[1]);
|
|
2456
2510
|
try log.boost(foe_ident, .SpecialAttack, -1);
|
|
2457
2511
|
try log.boost(foe_ident, .SpecialDefense, -1);
|
|
@@ -2470,7 +2524,8 @@ pub const Effects = struct {
|
|
|
2470
2524
|
}
|
|
2471
2525
|
};
|
|
2472
2526
|
|
|
2473
|
-
fn unmodifiedStats(battle: anytype,
|
|
2527
|
+
fn unmodifiedStats(battle: anytype, who: Player) *Stats(u16) {
|
|
2528
|
+
const side = battle.side(who);
|
|
2474
2529
|
if (!side.active.volatiles.Transform) return &side.stored().stats;
|
|
2475
2530
|
const id = ID.from(side.active.volatiles.transform);
|
|
2476
2531
|
return &battle.side(id.player).pokemon[id.id - 1].stats;
|
|
@@ -2536,140 +2591,80 @@ fn clearVolatiles(battle: anytype, who: Player, log: anytype) !void {
|
|
|
2536
2591
|
}
|
|
2537
2592
|
}
|
|
2538
2593
|
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
while (true) {
|
|
2553
|
-
const r = std.math.rotr(u8, battle.rng.next(), 1);
|
|
2554
|
-
if (r >= 217) return r;
|
|
2594
|
+
fn disabled(side: *Side, ident: ID, log: anytype) !bool {
|
|
2595
|
+
if (side.active.volatiles.disabled_move != 0) {
|
|
2596
|
+
// A Pokémon that transforms after being disabled may end up with less move slots
|
|
2597
|
+
const m = side.active.moves[side.active.volatiles.disabled_move - 1].id;
|
|
2598
|
+
// side.last_selected_move can be Struggle here on Pokemon Showdown we need an extra check
|
|
2599
|
+
const last = if (showdown and m == .Bide and side.active.volatiles.Bide)
|
|
2600
|
+
m
|
|
2601
|
+
else
|
|
2602
|
+
side.last_selected_move;
|
|
2603
|
+
if (m != .None and m == last) {
|
|
2604
|
+
side.active.volatiles.Charging = false;
|
|
2605
|
+
try log.disabled(ident, last);
|
|
2606
|
+
return true;
|
|
2555
2607
|
}
|
|
2556
2608
|
}
|
|
2609
|
+
return false;
|
|
2610
|
+
}
|
|
2557
2611
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
inline fn confusionDuration(battle: anytype) u3 {
|
|
2564
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 2, 6));
|
|
2565
|
-
return @intCast(u3, (battle.rng.next() & 3) + 2);
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
inline fn confused(battle: anytype) bool {
|
|
2569
|
-
if (showdown) return !battle.rng.chance(u8, 128, 256);
|
|
2570
|
-
return battle.rng.next() >= Gen12.percent(50) + 1;
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
inline fn paralyzed(battle: anytype) bool {
|
|
2574
|
-
if (showdown) return battle.rng.chance(u8, 63, 256);
|
|
2575
|
-
return battle.rng.next() < Gen12.percent(25);
|
|
2612
|
+
inline fn buildRage(battle: anytype, who: Player, log: anytype) !void {
|
|
2613
|
+
const side = battle.side(who);
|
|
2614
|
+
if (side.active.volatiles.Rage and side.active.boosts.atk < 6) {
|
|
2615
|
+
try Effects.boost(battle, who, Move.get(.Rage), log);
|
|
2576
2616
|
}
|
|
2617
|
+
}
|
|
2577
2618
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2619
|
+
fn handleThrashing(battle: anytype, active: *ActivePokemon) bool {
|
|
2620
|
+
var volatiles = &active.volatiles;
|
|
2621
|
+
assert(volatiles.Thrashing);
|
|
2622
|
+
if (volatiles.attacks > 0) return false;
|
|
2582
2623
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2624
|
+
volatiles.Thrashing = false;
|
|
2625
|
+
volatiles.Confusion = true;
|
|
2626
|
+
volatiles.confusion = @intCast(u3, if (showdown)
|
|
2627
|
+
battle.rng.range(u8, 2, 6)
|
|
2628
|
+
else
|
|
2629
|
+
(battle.rng.next() & 3) + 2);
|
|
2587
2630
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
return battle.rng.next() < 1 + (if (low) Gen12.percent(20) else Gen12.percent(40));
|
|
2591
|
-
}
|
|
2631
|
+
return true;
|
|
2632
|
+
}
|
|
2592
2633
|
|
|
2593
|
-
|
|
2594
|
-
if (showdown) return @intCast(u3, battle.rng.range(u8, 1, 8));
|
|
2595
|
-
while (true) {
|
|
2596
|
-
const r = battle.rng.next() & 7;
|
|
2597
|
-
if (r != 0) return @intCast(u3, r);
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2634
|
+
const DISTRIBUTION = [_]u3{ 2, 2, 2, 3, 3, 3, 4, 5 };
|
|
2600
2635
|
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2636
|
+
fn distribution(battle: anytype) u3 {
|
|
2637
|
+
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2638
|
+
const r = (battle.rng.next() & 3);
|
|
2639
|
+
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2640
|
+
}
|
|
2605
2641
|
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
if (
|
|
2621
|
-
r -= 1;
|
|
2622
|
-
if (r == 0) break;
|
|
2623
|
-
}
|
|
2642
|
+
fn randomMoveSlot(rand: anytype, moves: []MoveSlot, check_pp: u4) u4 {
|
|
2643
|
+
if (showdown) {
|
|
2644
|
+
if (check_pp == 0) {
|
|
2645
|
+
var i: usize = moves.len;
|
|
2646
|
+
while (i > 0) {
|
|
2647
|
+
i -= 1;
|
|
2648
|
+
if (moves[i].id != .None) return rand.range(u4, 0, @intCast(u4, i + 1)) + 1;
|
|
2649
|
+
}
|
|
2650
|
+
} else {
|
|
2651
|
+
var r = rand.range(u4, 0, @intCast(u4, check_pp)) + 1;
|
|
2652
|
+
var i: usize = 0;
|
|
2653
|
+
while (i < moves.len and r > 0) : (i += 1) {
|
|
2654
|
+
if (moves[i].pp > 0) {
|
|
2655
|
+
r -= 1;
|
|
2656
|
+
if (r == 0) break;
|
|
2624
2657
|
}
|
|
2625
|
-
return @intCast(u4, i + 1);
|
|
2626
2658
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
while (true) {
|
|
2630
|
-
const r = @intCast(u4, battle.rng.next() & 3);
|
|
2631
|
-
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
inline fn disableDuration(battle: anytype) u4 {
|
|
2636
|
-
if (showdown) return @intCast(u4, battle.rng.range(u8, 1, 9));
|
|
2637
|
-
return @intCast(u4, (battle.rng.next() & 7) + 1);
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
inline fn metronome(battle: anytype) Move {
|
|
2641
|
-
if (showdown) {
|
|
2642
|
-
const r = battle.rng.range(u8, 0, @enumToInt(Move.Struggle) - 2);
|
|
2643
|
-
const mod = @as(u2, (if (r < @enumToInt(Move.Metronome) - 1) 1 else 2));
|
|
2644
|
-
return @intToEnum(Move, r + mod);
|
|
2645
|
-
}
|
|
2646
|
-
while (true) {
|
|
2647
|
-
const r = battle.rng.next();
|
|
2648
|
-
if (r == 0 or r == @enumToInt(Move.Metronome)) continue;
|
|
2649
|
-
if (r >= @enumToInt(Move.Struggle)) continue;
|
|
2650
|
-
return @intToEnum(Move, r);
|
|
2659
|
+
return @intCast(u4, i + 1);
|
|
2651
2660
|
}
|
|
2652
2661
|
}
|
|
2653
2662
|
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
const r = battle.rng.next();
|
|
2658
|
-
if (r < max) return r;
|
|
2659
|
-
}
|
|
2663
|
+
while (true) {
|
|
2664
|
+
const r = @intCast(u4, rand.next() & 3);
|
|
2665
|
+
if (moves[r].id != .None and (check_pp == 0 or moves[r].pp > 0)) return r + 1;
|
|
2660
2666
|
}
|
|
2661
|
-
|
|
2662
|
-
inline fn distribution(battle: anytype) u3 {
|
|
2663
|
-
if (showdown) return DISTRIBUTION[battle.rng.range(u8, 0, DISTRIBUTION.len)];
|
|
2664
|
-
const r = (battle.rng.next() & 3);
|
|
2665
|
-
return @intCast(u3, (if (r < 2) r else battle.rng.next() & 3) + 2);
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
inline fn unboost(battle: anytype) bool {
|
|
2669
|
-
if (showdown) return battle.rng.chance(u8, 85, 256);
|
|
2670
|
-
return battle.rng.next() < Gen12.percent(33) + 1;
|
|
2671
|
-
}
|
|
2672
|
-
};
|
|
2667
|
+
}
|
|
2673
2668
|
|
|
2674
2669
|
test "RNG agreement" {
|
|
2675
2670
|
if (!showdown) return;
|
|
@@ -2769,7 +2764,7 @@ pub fn choices(battle: anytype, player: Player, request: Choice.Type, out: []Cho
|
|
|
2769
2764
|
while (slot <= 4) : (slot += 1) {
|
|
2770
2765
|
const m = active.moves[slot - 1];
|
|
2771
2766
|
if (m.id == .None) break;
|
|
2772
|
-
if (m.id ==
|
|
2767
|
+
if (m.id == side.last_selected_move) {
|
|
2773
2768
|
// Pokémon Showdown displays Struggle if limited to Bide but unable to pick
|
|
2774
2769
|
const struggle =
|
|
2775
2770
|
m.id == .Bide and (m.pp == 0 or active.volatiles.disabled_move == slot);
|