修复PVP地形被切小岛/出生点干预、药水NBT缺失,以及商店附魔书NBT解析

- PVPArenaManager: 删除forceTerraformSpawnArea等184行小岛死代码;新增deleteExistingPVPWorldFolder确保每次进入都是自然生成的纯净主世界(彻底消除旧版setSpawnLocation/小岛误加载);pvpEquipment与makePotion改用PotionMeta写入INSTANT_HEALTH真实治疗NBT(1.21.4 PotionType.HEALING)
- ShopManager: 新增ENCHANT_MAP与inferEnchantedBook,按中文名解析附魔等级并写入EnchantmentStorageMeta,商店附魔书现在带正确附魔NBT
This commit is contained in:
2026-07-08 01:37:46 +08:00
parent 3c3d7cdbf2
commit 3c15e6b060
2 changed files with 1731 additions and 1716 deletions
+49 -192
View File
@@ -14,6 +14,7 @@ import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.io.BukkitObjectInputStream;
@@ -175,12 +176,18 @@ public class PVPArenaManager implements Listener {
// 金苹果 x 8
pvpEquipment.add(new ItemStack(Material.GOLDEN_APPLE, 8));
// 药水
// 药水(带真实治疗效果 NBT)
ItemStack potion = new ItemStack(Material.POTION, 3);
ItemMeta potionMeta = potion.getItemMeta();
if (potionMeta != null) {
potionMeta.setDisplayName("§a§l瞬间治疗药水");
potion.setItemMeta(potionMeta);
if (potionMeta instanceof PotionMeta) {
PotionMeta pMeta = (PotionMeta) potionMeta;
pMeta.setDisplayName("§a§l瞬间治疗药水");
pMeta.addCustomEffect(new org.bukkit.potion.PotionEffect(
org.bukkit.potion.PotionEffectType.INSTANT_HEALTH,
1, 0, false, false), true);
pMeta.setBasePotionData(new org.bukkit.potion.PotionData(
org.bukkit.potion.PotionType.HEALING, false, false));
potion.setItemMeta(pMeta);
}
pvpEquipment.add(potion);
}
@@ -237,6 +244,11 @@ public class PVPArenaManager implements Listener {
// ★ 不设 seed:让 MC 自行随机决定地形种子(插件对地形零干预)。
// 创建后通过 getSeed() 读取 MC 选定的种子并记录日志,便于排查"出生点脚下是水"的失败种子。
// ★ 彻底清理磁盘上可能残留的旧世界目录:旧版本曾干预出生点(setSpawnLocation)并强制造陆,
// 若上次删除因 Windows 文件锁失败,createWorld 会误加载到旧世界(出生点被锁定/小岛地形)。
// 这里重试删除确保目录消失,保证每次进入都是 MC 自然生成的全新主世界与自然出生点。
deleteExistingPVPWorldFolder();
World pvpWorld = creator.createWorld();
long dt = System.currentTimeMillis() - t0;
if (pvpWorld == null) {
@@ -530,6 +542,25 @@ public class PVPArenaManager implements Listener {
plugin.getLogger().info("[PVP] PVP世界已删除,下次进入将随机重新生成地形");
}
/**
* 在创建全新 PVP 世界前,彻底删除磁盘上可能残留的旧世界目录。
* ★ 必要性:旧版本曾对出生点执行 setSpawnLocation 并对周边强制造陆(小岛),
* 若上次删除因 Windows 文件锁而失败,createWorld() 会误加载到该旧世界
* (出生点被锁定为 0,0、地形被切成小岛)。这里用重命名+重试确保目录真正消失,
* 保证每次进入都是 MC 自然生成的全新主世界,自然出生点完全由 MC 决定。
*/
private void deleteExistingPVPWorldFolder() {
File folder = new File(Bukkit.getWorldContainer(), PVP_WORLD_NAME);
if (!folder.exists()) return;
for (int i = 0; i < 5; i++) {
moveWorldToTrash(folder);
if (!folder.exists()) return;
try { Thread.sleep(300); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
}
// 极少数情况下重命名仍失败,退回递归删除
deleteWorldFolder(folder);
}
/**
* 强制同步生成出生点周边区块(半径 radius 个区块),确保地形已存在后再寻找安全出生点
* 必须在主线程调用(命令处理阶段)
@@ -540,7 +571,7 @@ public class PVPArenaManager implements Listener {
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
try {
// true=强制生成;再读取一个方块以触发区块填充,避免异步未就绪导致 findSafeSpawn 误判虚空
// true=强制生成;再读取一个方块以触发区块填充,避免异步未就绪导致出生点下方误判为虚空
world.loadChunk(cx + dx, cz + dz, true);
world.getChunkAt(cx + dx, cz + dz).getBlock(8, 64, 8).getType();
} catch (Exception e) {
@@ -550,99 +581,7 @@ public class PVPArenaManager implements Listener {
}
}
private int lastSpawnLandScore = -1; // findSafeSpawn 最后一次搜索的陆地质量分(供 createPVPWorld 判断是否重试)
/**
* 在世界中寻找安全的出生点(螺旋向外搜索最高的非空气且非流体方块作为地面)
*
* ★ 2026-07-07 改进:
* 1. 搜索半径扩大到 64 格(原32),覆盖更广范围找陆地
* 2. 新增"陆地优先"评分:草地/泥土/石头 > 沙子 > 其它固体 > 流体
* 3. 如果出生点周边全是水(海洋 biome),沿对角线远距离搜寻陆地
* 4. 即使找到的地面在水面上方(如水面树冠),也继续搜更好的陆地位置
*/
private Location findSafeSpawn(World world) {
int baseX = world.getSpawnLocation().getBlockX();
int baseZ = world.getSpawnLocation().getBlockZ();
final int SEARCH_RADIUS = 64; // 扩大搜索范围(原32格,海洋biome需要更大范围)
int bestX = baseX, bestZ = baseZ, bestY = -1;
int bestLandScore = -1; // 陆地质量分(越高越好)
// 第一轮:螺旋搜索,优先选高分陆地
for (int r = 0; r <= SEARCH_RADIUS; r++) {
for (int dx = -r; dx <= r; dx++) {
for (int dz = -r; dz <= r; dz++) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
int x = baseX + dx, z = baseZ + dz;
for (int y = 255; y >= 1; y--) {
Block block = world.getBlockAt(x, y, z);
Material type = block.getType();
if (!type.isAir() && !isFluid(type)) {
int landScore = getLandScore(type);
// 同等高度优先选陆地质量更高的;或更高位置的陆地
if (y > bestY || (y == bestY && landScore > bestLandScore)) {
bestY = y; bestX = x; bestZ = z;
bestLandScore = landScore;
}
break;
}
}
}
}
// 找到高质量陆地(非沙地)就提前结束,不需要搜完全部范围
if (bestY >= 0 && bestLandScore >= 2) break;
}
// 第二轮:如果第一轮只找到低分地面(沙地/水下固体),尝试沿8个方向远距离扫掠找真正陆地
if (bestY >= 0 && bestLandScore < 2) {
plugin.getLogger().info("[PVP] 出生点周边质量较低(分数=" + bestLandScore + "),启动远距离陆地搜索...");
int[][] directions = {{1,1},{1,-1},{-1,1},{-1,-1},{1,0},{-1,0},{0,1},{0,-1}};
for (int[] dir : directions) {
for (int dist = SEARCH_RADIUS + 16; dist <= SEARCH_RADIUS + 128; dist += 16) {
int tx = baseX + dir[0] * dist;
int tz = baseZ + dir[1] * dist;
for (int y = 255; y >= 1; y--) {
Block block = world.getBlockAt(tx, y, tz);
Material type = block.getType();
if (!type.isAir() && !isFluid(type)) {
int landScore = getLandScore(type);
if (landScore >= 2 && y > bestY) {
bestY = y; bestX = tx; bestZ = tz;
bestLandScore = landScore;
plugin.getLogger().info("[PVP] 远距离搜索发现陆地: (" + tx + "," + bestY + "," + tz + ") 分数=" + landScore);
}
break;
}
}
}
if (bestLandScore >= 2) break; // 任一方向找到好陆地即可
}
}
if (bestY >= 0) {
lastSpawnLandScore = bestLandScore;
return new Location(world, bestX + 0.5, bestY + 1, bestZ + 0.5);
}
lastSpawnLandScore = -1; // 未找到安全地面(疑似虚空世界)
return null;
}
/**
* 陆地质量评分:用于选择最佳出生点
* 3=优质陆地(草/泥土/石头) | 2=可接受(沙子/沙砾) | 1=低分(其它固体) | -1=流体/空气
*/
private int getLandScore(Material type) {
switch (type.name()) {
case "GRASS_BLOCK": case "DIRT": case "STONE": case "COBBLESTONE":
case "PODZOL": case "MYCELIUM": case "FARMLAND":
return 3; // ★ 优质陆地——最适合做出生点
case "SAND": case "GRAVEL": case "SANDSTONE":
return 2; // 可接受——沙滩/河岸
default:
if (!type.isAir() && !isFluid(type)) return 1; // 其它固体方块
return -1;
}
}
/**
* 判断方块是否为流体(水/熔岩),流体上不适合作为安全出生地面
@@ -651,31 +590,6 @@ public class PVPArenaManager implements Listener {
return type == Material.WATER || type == Material.LAVA;
}
/**
* 保底平台:如果出生点下方全是空气则铺一个平台
* ★ 改进:扩大到20x20 + 四角萤石标记 + 合理高度(y=120)
*/
private void buildFallbackPlatform(World world) {
int centerY = 120;
for (int dx = -10; dx <= 10; dx++) {
for (int dz = -10; dz <= 10; dz++) {
world.getBlockAt(dx, centerY, dz).setType(Material.STONE);
world.getBlockAt(dx, centerY - 1, dz).setType(Material.STONE);
// 边缘一圈加围栏防止掉落
if (Math.abs(dx) == 10 || Math.abs(dz) == 10) {
world.getBlockAt(dx, centerY + 1, dz).setType(Material.OAK_FENCE);
}
}
}
// 四角萤石标记(夜间可见)
world.getBlockAt(-10, centerY + 2, -10).setType(Material.GLOWSTONE);
world.getBlockAt(10, centerY + 2, -10).setType(Material.GLOWSTONE);
world.getBlockAt(-10, centerY + 2, 10).setType(Material.GLOWSTONE);
world.getBlockAt(10, centerY + 2, 10).setType(Material.GLOWSTONE);
// 中央标记
world.getBlockAt(0, centerY + 1, 0).setType(Material.GLOWSTONE);
plugin.getLogger().warning("[PVP] 已生成保底出生平台: 中心(0," + (centerY+1) + ",0), 范围20x20");
}
/**
* 获取指定列从顶部向下的第一个非空气、非流体方块 Y 坐标(用于确定出生点地形高度)
@@ -688,73 +602,6 @@ public class PVPArenaManager implements Listener {
return 64;
}
/**
* ★ 强制造陆(最后兜底):当连续多个随机种子都生成海洋/虚空时,
* 在出生点周围强制改造出一块自然主世界风格的开阔陆地
* (草地平原 + 缓丘 + 稀疏树木),彻底告别 20x20 保底小平台。
*
* 设计要点:
* - 半径 40 格(81x81)大区域,足够玩家活动与战斗
* - 用正弦叠加做缓丘,避免纯平面呆板
* - 外圈 6 格缓坡沉入水面下,形成自然岛屿感
* - 稀疏随机树木点缀
*
* @param world 已创建但地形不合格的PVP世界
*/
private void forceTerraformSpawnArea(World world) {
Location spawn = world.getSpawnLocation();
int cx = spawn.getBlockX();
int cz = spawn.getBlockZ();
final int R = 40; // 半径40格 → 81x81 开阔地形
final int GROUND_Y = 64; // 主世界海平面附近
Random rng = new Random(System.nanoTime() ^ (cx * 31L + cz));
for (int dx = -R; dx <= R; dx++) {
for (int dz = -R; dz <= R; dz++) {
int x = cx + dx, z = cz + dz;
// 正弦叠加生成缓丘(自然起伏)
double hill = Math.sin(dx * 0.08) * 2.0 + Math.cos(dz * 0.07) * 2.0;
int topY = GROUND_Y + (int) Math.round(hill);
// 外圈 6 格缓坡下沉入水,形成岛屿感
int dist = Math.max(Math.abs(dx), Math.abs(dz));
if (dist > R - 6) topY -= (dist - (R - 6));
// 填充地层:表面草 / 上两层泥 / 更深处石
for (int y = topY; y >= topY - 5; y--) {
if (y < 1) break;
Material m;
if (y == topY) m = Material.GRASS_BLOCK;
else if (y >= topY - 2) m = Material.DIRT;
else m = Material.STONE;
world.getBlockAt(x, y, z).setType(m);
}
// 稀疏树木点缀(约 1/140 概率,且只在接近海平面的地块)
if (topY >= GROUND_Y - 1 && topY <= GROUND_Y + 3 && rng.nextInt(140) == 0) {
plantOakTree(world, x, topY + 1, z);
}
}
}
// 重置出生点到正中央草地上方
world.setSpawnLocation(cx, GROUND_Y + 2, cz);
plugin.getLogger().info("[PVP] 强制造陆完成: 出生点周边 " + (R * 2 + 1) + "x" + (R * 2 + 1)
+ " 草地平原已生成(最后兜底,正常情况不应触发)");
}
/** 在世界中种一棵简单橡树(树干4格 + 叶冠),仅用于强制造陆点缀 */
private void plantOakTree(World world, int x, int y, int z) {
for (int i = 0; i < 4; i++) {
world.getBlockAt(x, y + i, z).setType(Material.OAK_LOG);
}
for (int dy = 3; dy <= 5; dy++) {
for (int dx = -2; dx <= 2; dx++) {
for (int dz = -2; dz <= 2; dz++) {
if (Math.abs(dx) == 2 && Math.abs(dz) == 2) continue; // 去四角更圆润
if (dy == 5 && (Math.abs(dx) > 1 || Math.abs(dz) > 1)) continue; // 树冠顶部收窄
Block b = world.getBlockAt(x + dx, y + dy, z + dz);
if (b.getType().isAir()) b.setType(Material.OAK_LEAVES);
}
}
}
}
// ==================== GUI关闭事件(防止ESC跳过装备选择)====================
@@ -1419,9 +1266,19 @@ public class PVPArenaManager implements Listener {
private ItemStack makePotion(int count) {
ItemStack potion = new ItemStack(Material.POTION, count);
ItemMeta meta = potion.getItemMeta();
if (meta != null) {
meta.setDisplayName("§a§l瞬间治疗药水");
potion.setItemMeta(meta);
if (meta instanceof PotionMeta) {
PotionMeta pMeta = (PotionMeta) meta;
pMeta.setDisplayName("§a§l瞬间治疗药水");
// ★ 写入真实药水效果 NBT:瞬间治疗(数量越多档位越高效果越强),
// 否则客户端只显示"药水"却没有任何效果(玩家反馈"药水无效果")。
pMeta.addCustomEffect(new org.bukkit.potion.PotionEffect(
org.bukkit.potion.PotionEffectType.INSTANT_HEALTH,
1, // 瞬间效果,duration 无意义
count >= 8 ? 1 : 0, // 下合金档(8瓶)→治疗II,其余治疗I
false, false), true);
pMeta.setBasePotionData(new org.bukkit.potion.PotionData(
org.bukkit.potion.PotionType.HEALING, false, false));
potion.setItemMeta(pMeta);
}
return potion;
}
+158
View File
@@ -22,6 +22,9 @@ import org.bukkit.util.io.BukkitObjectOutputStream;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.NamespacedKey;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
@@ -128,6 +131,53 @@ public class ShopManager implements Listener {
EFFECT_MAP.put("神龟", PotionEffectType.RESISTANCE);
}
// ===== 附魔书:中文附魔名 → Bukkit Enchantment =====
private static final Map<String, Enchantment> ENCHANT_MAP = new HashMap<>();
static {
regEnchant("锋利", "sharpness");
regEnchant("亡灵杀手", "smite");
regEnchant("节肢杀手", "bane_of_arthropods");
regEnchant("击退", "knockback");
regEnchant("火焰附加", "fire_aspect");
regEnchant("掠夺", "looting");
regEnchant("横扫之刃", "sweeping_edge");
regEnchant("保护", "protection");
regEnchant("火焰保护", "fire_protection");
regEnchant("摔落保护", "feather_falling");
regEnchant("爆炸保护", "blast_protection");
regEnchant("弹射物保护", "projectile_protection");
regEnchant("水下呼吸", "respiration");
regEnchant("深海探索者", "depth_strider");
regEnchant("冰霜行者", "frost_walker");
regEnchant("荆棘", "thorns");
regEnchant("精准采集", "silk_touch");
regEnchant("效率", "efficiency");
regEnchant("耐久", "unbreaking");
regEnchant("时运", "fortune");
regEnchant("经验修补", "mending");
regEnchant("力量", "power");
regEnchant("冲击", "punch");
regEnchant("火焰", "flame");
regEnchant("无限", "infinity");
regEnchant("穿透", "piercing");
regEnchant("激流", "riptide");
regEnchant("忠诚", "loyalty");
regEnchant("引雷", "channeling");
regEnchant("多重射击", "multishot");
regEnchant("快速装填", "quick_charge");
regEnchant("海之眷顾", "luck_of_the_sea");
regEnchant("饵钓", "lure");
regEnchant("绑定诅咒", "binding_curse");
regEnchant("消失诅咒", "vanishing_curse");
regEnchant("穿刺", "impaling");
}
private static void regEnchant(String cn, String key) {
try {
Enchantment e = Enchantment.getByKey(NamespacedKey.minecraft(key));
if (e != null) ENCHANT_MAP.put(cn, e);
} catch (Exception ignored) {}
}
public static class CartItem {
String itemId;
String displayName;
@@ -1303,8 +1353,116 @@ public class ShopManager implements Listener {
* "强效力量喷溅药水" + SPLASH_POTION
* → PotionEffect(STRENGTH, 600, 1)
*/
// ===== 附魔书:由显示名推断存储附魔 =====
private static final java.util.regex.Pattern ROMAN_PAT =
java.util.regex.Pattern.compile("(?i)\\b(X{0,3}(?:IX|IV|V?I{1,3}))\\b");
private static final java.util.regex.Pattern ARABIC_PAT =
java.util.regex.Pattern.compile("\\d+");
private static final java.util.regex.Pattern CN_PAT =
java.util.regex.Pattern.compile("[零一二三四五六七八九十百]+");
private ItemStack inferEnchantedBook(String name) {
try {
ItemStack book = new ItemStack(Material.ENCHANTED_BOOK);
EnchantmentStorageMeta meta = (EnchantmentStorageMeta) book.getItemMeta();
if (meta == null) return null;
boolean added = false;
for (Map.Entry<String, Enchantment> e : ENCHANT_MAP.entrySet()) {
if (!name.contains(e.getKey())) continue;
// 若已有更具体的附魔词包含本词(如"火焰保护"命中时跳过"保护"),避免重复计入
boolean shadowed = false;
for (Map.Entry<String, Enchantment> o : ENCHANT_MAP.entrySet()) {
if (o != e && o.getKey().length() > e.getKey().length()
&& o.getKey().contains(e.getKey()) && name.contains(o.getKey())) {
shadowed = true;
break;
}
}
if (shadowed) continue;
int lvl = parseEnchantLevel(name, e.getKey());
meta.addStoredEnchant(e.getValue(), lvl, true);
added = true;
}
if (!added) return null;
book.setItemMeta(meta);
return book;
} catch (Exception ex) {
return null;
}
}
private int parseEnchantLevel(String name, String keyword) {
int idx = name.indexOf(keyword);
if (idx < 0) return 1;
String after = org.bukkit.ChatColor.stripColor(
name.substring(idx + keyword.length()));
java.util.regex.Matcher m = ROMAN_PAT.matcher(after);
if (m.find()) return romanToInt(m.group());
java.util.regex.Matcher m2 = ARABIC_PAT.matcher(after);
if (m2.find()) {
try { return Integer.parseInt(m2.group()); } catch (NumberFormatException ignored) {}
}
java.util.regex.Matcher m3 = CN_PAT.matcher(after);
if (m3.find()) return cnToInt(m3.group());
return 1;
}
private int romanToInt(String s) {
s = s.toUpperCase();
int total = 0, prev = 0;
for (int i = s.length() - 1; i >= 0; i--) {
int v;
switch (s.charAt(i)) {
case 'I': v = 1; break;
case 'V': v = 5; break;
case 'X': v = 10; break;
case 'L': v = 50; break;
case 'C': v = 100; break;
default: v = 0;
}
total += (v < prev) ? -v : v;
prev = v;
}
return Math.max(1, total);
}
private int cnToInt(String s) {
int total = 0, num = 0;
for (int i = 0; i < s.length(); i++) {
int d;
switch (s.charAt(i)) {
case '零': d = 0; break;
case '一': d = 1; break;
case '二': d = 2; break;
case '三': d = 3; break;
case '四': d = 4; break;
case '五': d = 5; break;
case '六': d = 6; break;
case '七': d = 7; break;
case '八': d = 8; break;
case '九': d = 9; break;
case '十': d = 10; break;
case '百': d = 100; break;
default: d = 0;
}
if (d == 10 || d == 100) {
num = (num == 0 ? 1 : num) * d;
total += num;
num = 0;
} else {
num = d;
}
}
total += num;
return Math.max(1, total);
}
private ItemStack inferFromName(
String name, Material mat) {
// 附魔书:解析名称中的附魔词与等级,写入 EnchantmentStorageMeta
if (mat == Material.ENCHANTED_BOOK) {
return inferEnchantedBook(name);
}
// 只处理药水和箭
if (mat != Material.POTION
&& mat != Material.SPLASH_POTION