package Sdf1_login; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import java.util.regex.Matcher; import java.util.regex.Pattern; public class QuestTracker { private final Plugin plugin; private final PlayerStorage storage; private final Map playerBiome = new HashMap<>(); private final Map> questCache = new HashMap<>(); private final List checkInRecords = new ArrayList<>(); private final Map lastCheckInPos = new HashMap<>(); public void recordCheckIn( String player, String biomeKey, String category, String worldName, double x, double y, double z) { String record = player + "|" + worldName + "|" + biomeKey + "|" + category + "|" + String.format( "%.1f", x) + "|" + String.format( "%.1f", y) + "|" + String.format( "%.1f", z) + "|" + System.currentTimeMillis(); checkInRecords.add(record); plugin.getLogger().info( "[QuestTracker] 打卡记录: " + record); } public List getCheckInRecords() { return new ArrayList<>(checkInRecords); } public List getPlayerRecords( String player) { List list = new ArrayList<>(); for (String r : checkInRecords) { if (r.startsWith( player.toLowerCase() + "|")) { list.add(r); } } return list; } public QuestTracker(Plugin plugin) { this.plugin = plugin; this.storage = new PlayerStorage( plugin.getDataFolder(), plugin.getLogger()); loadAllQuests(); } public void debugPlayer(String player) { plugin.getLogger().info( "[QuestTracker] === " + player + " 任务数据 ==="); } public void shutdown() { if (storage != null) { storage.close(); } } /** * 玩家移动时记录当前群系 */ public void onPlayerMove( String player, String biomeName) { playerBiome.put(player, biomeName); } /** * 标记玩家已打卡该群系 */ // ========== 群系分类映射表 ========== private static final Map BIOME_CATEGORY = new HashMap<>(); static { // 海洋群系 BIOME_CATEGORY.put("minecraft:ocean", "海洋"); BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); // 平原群系 BIOME_CATEGORY.put("minecraft:plains", "平原"); BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); BIOME_CATEGORY.put("minecraft:meadow", "平原"); // 森林群系 BIOME_CATEGORY.put("minecraft:forest", "森林"); BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); BIOME_CATEGORY.put("minecraft:taiga", "森林"); BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); BIOME_CATEGORY.put("minecraft:jungle", "森林"); BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); // 沙漠群系 BIOME_CATEGORY.put("minecraft:desert", "沙漠"); BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); BIOME_CATEGORY.put("minecraft:beach", "沙漠"); BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); // 恶地群系 BIOME_CATEGORY.put("minecraft:badlands", "恶地"); BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); // 雪原群系 BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); BIOME_CATEGORY.put("minecraft:grove", "雪原"); BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); // 沙砾群系 BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); // 沼泽群系 BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); // 河流 BIOME_CATEGORY.put("minecraft:river", "河流"); // 蘑菇岛 BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); // 地下群系 BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); // 下界群系 BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); // 末地群系 BIOME_CATEGORY.put("minecraft:the_end", "末地"); BIOME_CATEGORY.put("minecraft:the_void", "末地"); BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); } // 所有群系大类列表 private static final String[] ALL_CATEGORIES = {"海洋", "平原", "森林", "沙漠", "恶地", "雪原", "沙砾", "沼泽", "河流", "蘑菇岛", "地下", "下界", "末地"}; // 所有群系分类的条件关键词映射 private static final Map CATEGORY_KEYWORDS = new HashMap<>(); static { CATEGORY_KEYWORDS.put("海洋", "海洋"); CATEGORY_KEYWORDS.put("平原", "平原"); CATEGORY_KEYWORDS.put("森林", "森林"); CATEGORY_KEYWORDS.put("沙漠", "沙漠"); CATEGORY_KEYWORDS.put("恶地", "恶地"); CATEGORY_KEYWORDS.put("雪原", "雪原"); CATEGORY_KEYWORDS.put("沙砾", "沙砾"); CATEGORY_KEYWORDS.put("沼泽", "沼泽"); CATEGORY_KEYWORDS.put("河流", "河流"); CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); CATEGORY_KEYWORDS.put("地下", "地下"); CATEGORY_KEYWORDS.put("下界", "下界"); CATEGORY_KEYWORDS.put("末地", "末地"); // 条件文本中的别名映射 CATEGORY_KEYWORDS.put("地狱", "下界"); CATEGORY_KEYWORDS.put("下届", "下界"); CATEGORY_KEYWORDS.put("主岛", "末地"); } /** * 用世界维度名分类(最可靠) * 世界名含 the_nether → 下界 * 世界名含 the_end → 末地 * 其余 → 主世界 */ public String mapBiomeCategory( String rawKey, String worldName) { // 下界维度优先用世界名 if (worldName != null) { String w = worldName.toLowerCase(); if (w.contains("nether")) return "下界"; if (w.contains("end")) return "末地"; } // 用群系映射表 if (rawKey != null) { String cat = BIOME_CATEGORY.get( rawKey.toLowerCase()); if (cat != null) return cat; } return "主世界"; } // ========== 解析为区块 ========== private List
parseIntoSections( List lines) { boolean hasHash = false; boolean hasDash = false; for (String l : lines) { String t = l.trim(); if (t.startsWith("#")) hasHash = true; if (t.startsWith("----") || t.startsWith("====") || t.startsWith("****")) hasDash = true; } if (hasHash) { return parseHashSections(lines); } else if (hasDash) { List converted = new ArrayList<>(); for (String l : lines) { String t = l.trim(); if (t.startsWith("----") || t.startsWith("====") || t.startsWith("****")) { String title = t .replaceAll( "^[-=*]+\\s*", "") .replaceAll( "\\s*[-=*]+$", "") .trim(); if (title.isEmpty()) continue; converted.add("# " + title); } else { converted.add(l); } } return parseHashSections(converted); } return new ArrayList<>(); } private List
parseHashSections( List lines) { List
sections = new ArrayList<>(); String curTitle = null; List curLines = new ArrayList<>(); for (String raw : lines) { String t = raw.trim(); if (t.startsWith("#")) { if (curTitle != null) { sections.add(new Section( curTitle, new ArrayList<>( curLines))); } curTitle = t.replaceFirst( "^#+\\s*", "").trim(); curLines = new ArrayList<>(); } else if (t.isEmpty()) { // skip } else { String cond = t.replaceFirst( "^-\\s+", "").trim(); if (!cond.isEmpty() && curTitle != null) { curLines.add(cond); } } } if (curTitle != null) { sections.add(new Section( curTitle, curLines)); } return sections; } private List
buildFlatSections( List lines, String category) { List conditions = new ArrayList<>(); for (String raw : lines) { String t = raw.trim(); if (t.isEmpty()) continue; String cond = t.replaceFirst( "^-\\s+", "").trim(); if (!cond.isEmpty()) { conditions.add(cond); } } if (conditions.isEmpty()) return new ArrayList<>(); List
list = new ArrayList<>(); list.add(new Section( "第一阶段", conditions)); return list; } // ========== 文件加载入口 ========== private void loadAllQuests() { loadCategory("新人任务"); loadCategory("主线任务"); loadCategory("支线任务"); } private void loadCategory(String name) { plugin.getLogger().info( "[QuestTracker] " + name + ": 扫描中..."); File folder = new File( plugin.getDataFolder(), name); String path = "plugins/Sdf1_login/" + name + "/"; if (!folder.exists()) { folder.mkdirs(); plugin.getLogger().warning( "[QuestTracker] 已创建目录: " + path); questCache.put(name, new ArrayList<>()); return; } File[] mdFiles = folder.listFiles( (d, n) -> n.endsWith(".md")); if (mdFiles == null || mdFiles.length == 0) { plugin.getLogger().warning( "[QuestTracker] " + path + " 下无 .md 文件"); questCache.put(name, new ArrayList<>()); return; } Arrays.sort(mdFiles); plugin.getLogger().info( "[QuestTracker] " + name + ": 发现 " + mdFiles.length + " 个文件,开始扫描..."); // 合并所有文件的阶段 TreeMap merged = new TreeMap<>(); int fileCount = 0; for (int fi = 0; fi < mdFiles.length; fi++) { File file = mdFiles[fi]; List lines = readFileLines(file); if (lines.isEmpty()) { plugin.getLogger().warning( "[QuestTracker] " + file.getName() + ": 空文件,跳过"); continue; } List
sections = parseIntoSections(lines); if (sections.isEmpty()) { sections = buildFlatSections( lines, name); } if (sections.isEmpty()) { plugin.getLogger().warning( "[QuestTracker] " + file.getName() + ": 无法识别,跳过"); continue; } // 每个文件独立处理阶段号 int defaultNum = fi + 1; buildSingleFileStages( sections, name, merged, defaultNum); fileCount++; } if (merged.isEmpty()) { plugin.getLogger().warning( "[QuestTracker] " + name + ": 无有效任务数据"); questCache.put(name, new ArrayList<>()); return; } // 日志 for (Map.Entry en : merged.entrySet()) { int num = en.getKey() >= 1000 ? en.getKey() - 999 : en.getKey(); plugin.getLogger().info( "[QuestTracker] " + name + " 阶段" + num + " \"" + en.getValue().displayName + "\" → 条件" + en.getValue() .conditions.size() + "项, 详情" + en.getValue() .details.size() + "项, 奖励" + en.getValue() .rewards.size() + "项"); } List list = new ArrayList<>( merged.values()); questCache.put(name, list); plugin.getLogger().info( "[QuestTracker] " + name + ": 解析完成 → " + list.size() + " 个阶段" + " (来自 " + fileCount + " 个文件)"); } private Integer extractStageNumber( String title) { if (title == null) return null; String t = title.toLowerCase().trim(); // 含阶段/stage/关/chapter 关键词 boolean isStage = t.contains("阶段") || t.contains("stage") || t.contains("关卡") || t.contains("章节") || t.contains("chapter"); if (isStage) { String digits = title .replaceAll("[^0-9]", ""); if (!digits.isEmpty()) { try { return Integer.parseInt(digits); } catch (NumberFormatException e) { // fall through } } return parseChineseNumber(title); } // 无关键词但以数字开头(如 "1. 挖掘") String trimmed = t .replaceAll("^[.、\\s]+", "") .trim(); if (trimmed.length() > 0 && Character.isDigit( trimmed.charAt(0))) { String num = trimmed .replaceAll("[^0-9]", ""); if (!num.isEmpty()) { try { return Integer.parseInt(num); } catch (NumberFormatException e) { // fall through } } } return null; } // ========== 领取记录 ========== /** * 标记已领取某阶段奖励 */ public void markClaimed( String player, QuestFile qf) { if (player == null || qf == null) return; // 用阶段编号代替显示名,保证稳定 int num = getStageNumber(qf); String key = "claimed_" + qf.fileName + "_" + num; storage.setCondition(player, key); } public boolean hasClaimed( String player, QuestFile qf) { if (player == null || qf == null) return false; int num = getStageNumber(qf); String key = "claimed_" + qf.fileName + "_" + num; return toBool( storage.getCondition( player, key)); } private int parseChineseNumber( String title) { String[] cn = {"一", "二", "三", "四", "五", "六", "七", "八", "九", "十"}; if (title.contains("十")) { for (int j = 0; j < 9; j++) { if (title.contains(cn[j])) { return 10 + j + 1; } } return 10; } for (int i = 0; i < 9; i++) { if (title.contains(cn[i])) { return i + 1; } } return 0; } // ========== md 解析核心 ========== /** * 读取文件所有行 */ private List readFileLines( File file) { List lines = new ArrayList<>(); try { BufferedReader br = new BufferedReader( new FileReader(file)); String line; while ((line = br.readLine()) != null) { lines.add(line); } br.close(); } catch (Exception e) { plugin.getLogger().warning( "[QuestTracker] 读取 " + file.getName() + " 失败: " + e.getMessage()); } return lines; } /** * 自动检测格式并解析单个文件 */ private List parseMdFile( File file, String category) { List lines = readFileLines(file); if (lines.isEmpty()) return new ArrayList<>(); // 检测分隔符 boolean hasHash = false; boolean hasDash = false; for (String l : lines) { String t = l.trim(); if (t.startsWith("#")) hasHash = true; if (t.startsWith("----") || t.startsWith("====") || t.startsWith("****")) hasDash = true; } if (hasHash) { // # 分隔格式 return parseSections( lines, category); } else if (hasDash) { // ---- 分隔格式:转换为 # 后复用 List converted = new ArrayList<>(); for (String l : lines) { String t = l.trim(); if (t.startsWith("----") || t.startsWith("====") || t.startsWith("****")) { String title = t .replaceAll( "^[-=*]+\\s*", "") .replaceAll( "\\s*[-=*]+$", "") .trim(); if (title.isEmpty()) title = "separator"; converted.add("# " + title); } else { converted.add(l); } } return parseSections( converted, category); } else { // 无分隔符:整文件一个阶段 return parseFlat(lines, category); } } /** * # 分隔格式 → 区块 → 阶段 */ private List parseSections( List lines, String category) { // 拆分为区块 List
sections = new ArrayList<>(); String curTitle = null; List curLines = new ArrayList<>(); for (String raw : lines) { String t = raw.trim(); // # 开头 → 新区块 if (t.startsWith("#")) { if (curTitle != null) { sections.add( new Section( curTitle, new ArrayList<>( curLines))); } curTitle = t.replaceFirst( "^#+\\s*", "").trim(); curLines = new ArrayList<>(); } // 空行/分隔线 → 跳过 else if (t.isEmpty() || t.startsWith("----") || t.startsWith("====") || t.startsWith("***")) { // skip } // 普通行 → 区块内容 else { String cond = t.replaceFirst( "^-\\s+", "").trim(); if (!cond.isEmpty()) { curLines.add(cond); } } } // 保存最后一个区块 if (curTitle != null) { sections.add( new Section( curTitle, curLines)); } // 直接转为 QuestFile 列表 List result = new ArrayList<>(); int num = 1; for (Section sec : sections) { QuestFile qf = new QuestFile(); qf.displayName = extractDisplayName( sec.title); qf.fileName = category; qf.conditions = new ArrayList<>(sec.lines); qf.details = new ArrayList<>(); qf.rewards = new ArrayList<>(); result.add(qf); num++; } return result; } /** * 无分隔符:整文件 = 一个阶段 */ private List parseFlat( List lines, String category) { List list = new ArrayList<>(); QuestFile qf = makeDefaultStage( category); for (String raw : lines) { String t = raw.trim(); if (t.isEmpty()) continue; String cond = t.replaceFirst( "^-\\s+", "").trim(); if (!cond.isEmpty()) { qf.conditions.add(cond); } } if (!qf.conditions.isEmpty()) { list.add(qf); } return list; } // ========== Vault 经济奖励发放 ========== /** * 发放阶段奖励 * 含经济内容的走 Vault API */ public void claimRewards( Player player, QuestFile qf) { if (qf == null || qf.rewards == null || qf.rewards.isEmpty()) { player.sendMessage( "§c无可领取的奖励"); return; } net.milkbowl.vault.economy.Economy econ = ((Main) plugin).getEconomy(); int sentCount = 0; for (String reward : qf.rewards) { String rt = reward.trim(); if (rt.isEmpty()) continue; // 检查可选奖励标记 if (hasOptionalMarker(rt)) { if (!areAllConditionsMet(player.getName(), qf)) player.sendMessage( "§7[可选奖励] " + "需完成所有可选条件: " + rt); continue; } // 按 + 分割子项 String[] parts = rt.split("[+,]"); for (String part : parts) { part = part.trim(); if (part.isEmpty()) continue; // 尝试发经济 double amt = extractEconomyAmount(part); if (amt > 0 && econ != null) { double before = econ.getBalance(player); econ.depositPlayer( player, amt); player.sendMessage( "§a§l[任务奖励] §f" + part + " §7(余额: §e" + String.format( "%.2f", before) + " §7→ §a" + String.format( "%.2f", econ.getBalance( player)) + "§7)"); // 通知提成系统 if (plugin instanceof Main) { ((Main) plugin) .getCommission() .payCommission( player .getName(), amt); } sentCount++; continue; } // 尝试发物品 java.util.List items = parseRewardItems(part); if (!items.isEmpty()) { for (ItemStack is : items) { player.getInventory() .addItem(is); } player.sendMessage( "§a§l[任务奖励] §f" + part); sentCount++; continue; } // 非经济非物品 → 仅消息 player.sendMessage( "§a§l[任务奖励] §f" + part); sentCount++; } } plugin.getLogger().info( "[QuestTracker] " + player.getName() + " 领取了 \"" + qf.displayName + "\" 共 " + sentCount + " 项奖励"); } /** * 检查奖励文本是否含可选标记 */ private boolean hasOptionalMarker( String text) { return text.contains("(可选)") || text.contains("(进阶)") || text.contains("【可选】") || text.contains("【进阶】"); } /** * 检查阶段所有条件(含可选)是否全部满足 * 用于决定可选奖励是否发放 */ private boolean areAllConditionsMet( String player, QuestFile qf) { if (qf.conditions == null || qf.conditions.isEmpty()) return true; for (String cond : qf.conditions) { if (!isConditionMet(player, cond)) return false; } return true; } /** * 从奖励文本中提取经济金额 * 支持: "500游戏币" "500" "金币500" * "$500" "1000-5000"(随机) "1000~5000" */ private double extractEconomyAmount( String text) { String t = text.replaceAll(" ", ""); // 必须包含经济关键词才算经济奖励 boolean hasEcon = t.contains("游戏币") || t.contains("金币") || t.contains("积分") || t.contains("块钱") || t.contains("元") || t.contains("$") || t.contains("经济") || t.contains("余额") || t.contains("发钱"); if (!hasEcon) return 0; // 范围: "1000-5000" 或 "1000~5000" Matcher rm = Pattern .compile("(\\d+)[-~](\\d+)") .matcher(t); if (rm.find()) { double min = Double.parseDouble( rm.group(1)); double max = Double.parseDouble( rm.group(2)); return min + Math.random() * (max - min); } // 纯数字(含 $ 前缀) Matcher nm = Pattern .compile("\\$?(\\d+(\\.\\d+)?)") .matcher(t); if (nm.find()) { return Double.parseDouble( nm.group(1)); } return 0; } /** * 解析奖励物品文本 * "皮革套装*1" → 4件皮革装备 * "木剑*1" → 1把木剑 * "钻石*64" → 64颗钻石 */ private List parseRewardItems( String text) { List result = new ArrayList<>(); // 去掉条件前缀 // 如 "完成注册登录:皮革套装*1" if (text.contains(":")) { text = text.substring( text.indexOf(":") + 1); } if (text.contains(":")) { text = text.substring( text.indexOf(":") + 1); } // 按 + 分割多个物品 String[] parts = text.split("[+,]"); for (String part : parts) { part = part.trim(); if (part.isEmpty()) continue; // 跳过经济文本 if (extractEconomyAmount(part) > 0) continue; int amount = 1; String itemName = part; // 解析 *N 数量 if (part.contains("*")) { String[] sp = part.split("\\*"); itemName = sp[0].trim(); try { amount = Integer.parseInt( sp[1].trim()); } catch (Exception e) { amount = 1; } } List mats = matchMaterials(itemName); for (Material m : mats) { int give = m.name() .contains("HELMET") || m.name() .contains("CHESTPLATE") || m.name() .contains("LEGGINGS") || m.name() .contains("BOOTS") ? 1 : amount; result.add( new ItemStack(m, give)); } } return result; } /** * 中文物品名 → Material 列表 * 套装返回多个部件 */ private List matchMaterials( String name) { List list = new ArrayList<>(); String n = name.toLowerCase().trim(); // 套装 if (n.contains("皮革套装") || n.contains("皮套装")) { list.add(Material.LEATHER_HELMET); list.add(Material.LEATHER_CHESTPLATE); list.add(Material.LEATHER_LEGGINGS); list.add(Material.LEATHER_BOOTS); return list; } if (n.contains("铁套装") || n.contains("铁甲")) { list.add(Material.IRON_HELMET); list.add(Material.IRON_CHESTPLATE); list.add(Material.IRON_LEGGINGS); list.add(Material.IRON_BOOTS); return list; } if (n.contains("钻石套装") || n.contains("钻甲")) { list.add(Material.DIAMOND_HELMET); list.add(Material.DIAMOND_CHESTPLATE); list.add(Material.DIAMOND_LEGGINGS); list.add(Material.DIAMOND_BOOTS); return list; } // 木制工具套装 if (n.contains("木制工具") || n.contains("木工具")) { list.add(Material.WOODEN_SWORD); list.add(Material.WOODEN_PICKAXE); list.add(Material.WOODEN_AXE); list.add(Material.WOODEN_SHOVEL); return list; } if (n.contains("石制工具") || n.contains("石工具")) { list.add(Material.STONE_SWORD); list.add(Material.STONE_PICKAXE); list.add(Material.STONE_AXE); list.add(Material.STONE_SHOVEL); return list; } if (n.contains("铁工具")) { list.add(Material.IRON_SWORD); list.add(Material.IRON_PICKAXE); list.add(Material.IRON_AXE); list.add(Material.IRON_SHOVEL); return list; } // 单个物品 if (n.contains("木剑")) list.add(Material.WOODEN_SWORD); else if (n.contains("木镐")) list.add(Material.WOODEN_PICKAXE); else if (n.contains("铁剑")) list.add(Material.IRON_SWORD); else if (n.contains("铁镐")) list.add(Material.IRON_PICKAXE); else if (n.contains("钻石剑")) list.add(Material.DIAMOND_SWORD); else if (n.contains("钻石镐")) list.add(Material.DIAMOND_PICKAXE); else if (n.contains("钻石")) list.add(Material.DIAMOND); else if (n.contains("铁锭")) list.add(Material.IRON_INGOT); else if (n.contains("金锭") || n.contains("金块")) list.add(Material.GOLD_INGOT); else if (n.contains("煤炭")) list.add(Material.COAL); else if (n.contains("木棍")) list.add(Material.STICK); else if (n.contains("弓")) list.add(Material.BOW); else if (n.contains("箭")) list.add(Material.ARROW); else if (n.contains("床")) list.add(Material.RED_BED); else { // 尝试直接匹配 Material 名 try { list.add(Material.valueOf( n.toUpperCase() .replaceAll("[^A-Z_]", ""))); } catch (Exception e) { // 未知物品 } } return list; } private QuestFile getOrCreateStage( TreeMap map, int key, String category) { if (!map.containsKey(key)) { QuestFile qf = new QuestFile(); qf.displayName = "阶段 " + key; qf.fileName = category; qf.conditions = new ArrayList<>(); qf.details = new ArrayList<>(); qf.rewards = new ArrayList<>(); map.put(key, qf); } return map.get(key); } /** * 单个文件的区块 → 阶段 * defaultStageNum: 文件无阶段标题时的默认编号 */ private void buildSingleFileStages( List
sections, String category, TreeMap stageMap, int defaultStageNum) { Integer curKey = null; for (Section sec : sections) { Integer stageNum = extractStageNumber(sec.title); if (stageNum != null) { curKey = stageNum; } else if (curKey == null) { curKey = defaultStageNum; } QuestFile qf = getOrCreateStage( stageMap, curKey, category); String displayName = extractDisplayName(sec.title); // 设置显示名 if (!displayName.isEmpty() && qf.displayName .startsWith("阶段 ")) { qf.displayName = displayName; } // 路由 if (stageNum != null && sec.lines.isEmpty()) { if (!displayName.isEmpty()) { qf.displayName = displayName; } } else if (isRewardTitle(sec.title)) { // 奖励 → 只进 rewards qf.rewards.addAll(sec.lines); } else if (isDetailsTitle(sec.title)) { // 详情 → 进 details + conditions qf.details.addAll(sec.lines); for (String line : sec.lines) { if (!qf.conditions .contains(line)) { qf.conditions.add(line); } } } else if (isConditionsTitle( sec.title)) { // 条件 → 只进 conditions qf.conditions.addAll(sec.lines); } else { // 其他 → 条件 qf.conditions.addAll(sec.lines); } } } /** * "主线任务-斗志昂扬" → "斗志昂扬" * "支线-钓鱼" → "钓鱼" * "阶段 1" → "阶段 1" * "第一阶段" → "第一阶段" */ private String extractDisplayName( String title) { if (title == null) return ""; // 按 - 或 — 或 – 分割 String[] parts = title.split( "\\s*[-—–]\\s*", 2); if (parts.length >= 2) { String right = parts[1].trim(); if (!right.isEmpty()) { return right; } } return title; } private DatabaseManager getDb() { return ((Main) plugin).getDb(); } private boolean isDetailsTitle( String title) { String t = title.toLowerCase(); return t.contains("任务详情") || t.contains("详情") || t.contains("任务说明") || t.contains("任务目标") || t.contains("description") || t.contains("任务要求"); } private boolean isConditionsTitle( String title) { String t = title.toLowerCase(); return t.contains("解锁条件") || t.contains("条件") || t.contains("前置条件") || t.contains("前置要求") || t.contains("解锁") || t.contains("condition"); } private void ensureStage( TreeMap map, String category, QuestFile current, int counter) { if (current != null) return; // 用大数字确保排在有编号阶段后面 int key = 1000 + counter; QuestFile qf = new QuestFile(); qf.displayName = "阶段 " + (counter + 1); qf.fileName = category; qf.conditions = new ArrayList<>(); qf.rewards = new ArrayList<>(); map.put(key, qf); // 更新调用方的引用(通过返回值) // 这里用 map 操作,current 无法直接更新 // 所以改用返回值方式 } /** * 自动将条件中的奖励内容移到 rewards */ private void separateRewards(QuestFile qf) { List remaining = new ArrayList<>(); for (String line : qf.conditions) { if (isRewardContent(line)) { if (!qf.rewards.contains(line)) { qf.rewards.add(line); } } else { remaining.add(line); } } qf.conditions = remaining; } private boolean checkStageDependency( String player, String condition) { Matcher m = Pattern.compile( "第([一二三四五六七八九十\\d]+)" + "阶段") .matcher(condition); while (m.find()) { String numStr = m.group(1); int stageNum = parseChineseNumber(numStr); if (stageNum == 0) { try { stageNum = Integer.parseInt(numStr); } catch (Exception e) { return false; } } boolean stageDone = false; for (List quests : questCache.values()) { for (QuestFile qf : quests) { Integer qfNum = extractStageNumber( qf.displayName); if (qfNum != null && qfNum == stageNum) { if (isStageComplete(player, qf.conditions)) { stageDone = true; } break; } } if (stageDone) break; } if (!stageDone) return false; } return true; } /** * 判断一行内容是否为奖励 */ private boolean isRewardContent( String line) { String l = line.toLowerCase(); // 含货币关键词 if (l.contains("游戏币") || l.contains("金币") || l.contains("积分") || l.contains("钻石") || l.contains("发钱") || l.contains("发放") || l.contains("经验") || l.contains("等级") || l.contains("经济") || l.contains("余额") || l.contains("块钱")) return true; // 纯数字+单位(如 "500游戏币") if (l.matches( ".*\\d+\\s*(游戏币|金币|元|积分|" + "钻石|块钱|经验|级|经济|余额).*")) return true; // $前缀(如 "$500") if (l.matches(".*\\$\\d+.*")) return true; // 范围格式(如 "1000-5000" 或 "1000~5000") if (l.matches(".*\\d+[-~]\\d+.*") && !l.contains("阶段") && !l.contains("stage")) return true; return false; } // ========== 标题分类 ========== private boolean isStageTitle( String title) { String t = title.toLowerCase(); return t.contains("阶段") || t.contains("stage") || t.contains("第一") || t.contains("第二") || t.contains("第三") || t.contains("第四") || t.contains("第五"); } private boolean isRewardTitle( String title) { String t = title.toLowerCase(); return t.contains("奖励") || t.contains("奖品") || t.contains("报酬") || t.contains("reward") || t.contains("奖赏") || t.contains("礼包内容") || t.contains("礼包") || t.contains("物品奖励"); } private QuestFile makeDefaultStage( String category) { QuestFile qf = new QuestFile(); qf.displayName = "第一阶段"; qf.fileName = category; qf.conditions = new ArrayList<>(); qf.rewards = new ArrayList<>(); return qf; } private static class Section { final String title; final List lines; Section(String title, List lines) { this.title = title; this.lines = lines; } } // ========== 公开查询 ========== public List getQuests( String category) { List list = questCache.get(category); return list != null ? list : new ArrayList<>(); } public int getCompletedStageCount( String player, List quests) { int count = 0; if (quests == null) return 0; for (QuestFile qf : quests) { if (isStageComplete(player, qf.conditions)) { count++; } } return count; } public QuestFile getCurrentStage( String player, List quests) { if (quests == null) return null; for (QuestFile qf : quests) { if (!isStageComplete( player, qf.conditions)) { return qf; } } return null; } public int getStageNumber( List quests, QuestFile qf) { if (quests == null || qf == null) return 0; return quests.indexOf(qf) + 1; } public int getStageNumber( QuestFile qf) { if (qf == null) return 0; List quests = getQuests(qf.fileName); return getStageNumber(quests, qf); } public boolean isStageCompleted( String player, QuestFile qf) { if (qf == null) return false; return isStageComplete( player, qf.conditions); } public List getUnmetConditions( String player, List conditions) { List unmet = new ArrayList<>(); if (conditions == null) return unmet; for (String cond : conditions) { if (isOptional(cond)) continue; if (!isConditionMet( player, cond)) { unmet.add( getProgress( player, cond)); } } return unmet; } public String getProgress( String player, String rawCond) { if (player == null || rawCond == null) return "§7未知"; String key = stripOptional(rawCond); // 打卡群系进度 if (key.contains("打卡") && key.contains("群系")) { String biome = extractBiome(key); if ("ALL".equals(biome)) { int total = getCounter( player, "biome_ALL"); return "§7群系打卡: §e" + total + "/1"; } if (!biome.isEmpty()) { int catCount = getCounter( player, "biomecat_" + biome); return "§7打卡 " + biome + "群系: §e" + catCount + "/1"; } int total = getCounter( player, "biome_ALL"); return "§7群系打卡: §e" + total + "/1"; } // 阶段依赖 if (key.contains("完成") && key.contains("阶段")) { boolean done = checkStageDependency( player, key); return done ? "§a✓ " + key : "§c✗ " + key; } // 注册登录(检查主数据库是否已注册) // 注册登录(检查主数据库是否已注册) if ("完成注册登录".equals(key)) { boolean done = getDb() != null && getDb().userExists( player); return done ? "§a✓ 完成注册登录" : "§c✗ 完成注册登录"; } // 绑定邮箱 if (key.contains("绑定Email") || key.contains("绑定邮箱")) { Object val = getDb().getField( player, "email"); boolean done = val != null && !val.toString().isEmpty(); return done ? "§a✓ 绑定Email" : "§c✗ 绑定Email"; } // 在线时间 if (key.contains("在线") && key.contains("小时")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; long cur = getCounterLong( player, "PLAYTIME"); return "§7在线: §e" + formatMs(cur) + "/" + needed + "小时"; } // 打招呼/发消息/聊天 if (key.contains("打招呼") || key.contains("打个招呼") || key.contains("发消息") || key.contains("聊天")) { int cur = getCounter( player, "CHAT"); return "§7聊天: §e" + cur + "/1"; } // 破坏/放置方块 if ((key.contains("破坏") || key.contains("放置")) && key.contains("方块")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; int total = getCounter( player, "MINE:ALL") + getCounter( player, "PLACE:ALL"); return "§7方块操作: §e" + total + "/" + needed; } // 连续登录 if (key.contains("连续") && key.contains("登录")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; Object val = getDb().getField( player, "checkin_streak"); int streak = val instanceof Number ? ((Number) val).intValue() : 0; return "§7连续登录: §e" + streak + "/" + needed + "天"; } // 挖矿 if (key.startsWith("MINE:")) { String[] parts = key.split(":"); if (parts.length >= 3) { int needed = parseInt(parts[2]); int cur = getCounter( player, "MINE:" + parts[1]); return "§7挖 " + parts[1] + ": §e" + cur + "/" + needed; } } // 击杀生物 if (key.startsWith("KILL:")) { String[] parts = key.split(":"); if (parts.length >= 3) { int needed = parseInt(parts[2]); int cur = getCounter( player, "KILL:" + parts[1]); return "§7杀 " + parts[1] + ": §e" + cur + "/" + needed; } } // 击杀玩家 if (key.startsWith("KILL_PLAYER")) { String[] parts = key.split(":"); int needed = parts.length >= 2 ? parseInt(parts[1]) : 1; int cur = getCounter( player, "KILL_PLAYER"); return "§7击杀玩家: §e" + cur + "/" + needed; } // 合成 if (key.startsWith("CRAFT")) { String[] parts = key.split(":"); int needed = parts.length >= 2 ? parseInt(parts[1]) : 1; int cur = getCounter( player, "CRAFT"); return "§7合成: §e" + cur + "/" + needed; } // 钓鱼 if (key.startsWith("FISH")) { String[] parts = key.split(":"); int needed = parts.length >= 2 ? parseInt(parts[1]) : 1; int cur = getCounter( player, "FISH"); return "§7钓鱼: §e" + cur + "/" + needed; } // 在线时长(毫秒) if (key.startsWith("PLAYTIME")) { String[] parts = key.split(":"); long needed = parts.length >= 2 ? Long.parseLong(parts[1]) : 0; long cur = getCounterLong( player, "PLAYTIME"); return "§7在线: §e" + formatMs(cur) + "/" + formatMs(needed); } // 上报村庄 if (key.contains("上报") && key.contains("村庄")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; int cur = getCounter( player, "REPORT_VILLAGE"); return "§7上报村庄: §e" + cur + "/" + needed; } // 探索村庄 if (key.contains("探索") && key.contains("村庄")) { int cur = getCounter( player, "REPORT_VILLAGE"); return "§7探索村庄: §e" + cur + "/1"; } // 近7天登录 if (key.contains("7天") || key.contains("七天")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; int streak = getIntField( player, "checkin_streak"); if (streak == 0) { streak = getIntField( player, "streak"); } if (streak == 0) { streak = getIntField( player, "total_checkin_days"); } return "§7近7天登录: §e" + streak + "/" + needed + "天"; } // 完成新手任务全部 if (key.contains("新手任务") && key.contains("全部")) { List quests = getQuests("新人任务"); int done = 0; for (QuestFile qf : quests) { if (isStageComplete( player, qf.conditions)) done++; } return "§7新手任务: §e" + done + "/" + quests.size(); } // 完成主线任务全部 if (key.contains("主线任务") && key.contains("全部")) { List quests = getQuests("主线任务"); int done = 0; for (QuestFile qf : quests) { if (isStageComplete( player, qf.conditions)) done++; } return "§7主线任务: §e" + done + "/" + quests.size(); } return "§7" + key; } private int getIntField( String player, String field) { try { Object val = getDb().getField( player, field); if (val instanceof Number) { return ((Number) val).intValue(); } if (val != null) { return Integer.parseInt( val.toString().trim()); } } catch (Exception e) { // 忽略 } return 0; } public List checkInBiome( String player, String rawBiomeKey, String worldName, double x, double y, double z) { List matched = new ArrayList<>(); // 32格去重 String posKey = player + "_lastPos"; String curPos = worldName + "|" + String.format( "%.0f", x / 32) + "|" + String.format( "%.0f", y / 32) + "|" + String.format( "%.0f", z / 32); String lastPos = lastCheckInPos.get(posKey); if (curPos.equals(lastPos)) { plugin.getLogger().info( "[QuestTracker] 打卡忽略: " + player + " 32格内重复"); return matched; } lastCheckInPos.put(posKey, curPos); String category = mapBiomeCategory( rawBiomeKey, worldName); // 记录总打卡 int allCur = getCounter( player, "biome_ALL"); setCounter(player, "biome_ALL", allCur + 1); // 记录分类打卡 if (!category.isEmpty() && !"主世界" .equals(category)) { String catKey = "biomecat_" + category; int catCur = getCounter( player, catKey); setCounter(player, catKey, catCur + 1); } // 记录具体群系 String bioKey = "biome_" + rawBiomeKey; int bioCur = getCounter( player, bioKey); setCounter(player, bioKey, bioCur + 1); // 检查所有任务条件是否匹配 List categories = Arrays.asList( "新人任务", "主线任务", "支线任务"); for (String cat : categories) { List quests = getQuests(cat); for (QuestFile qf : quests) { if (isStageCompleted( player, qf)) continue; for (String cond : qf.conditions) { if (isOptional(cond)) continue; String condKey = stripOptional(cond); if (!condKey .contains("打卡") || !condKey .contains("群系")) continue; String target = extractBiome(condKey); boolean hit = false; if ("ALL".equals(target) || "风景点" .equals(target)) { hit = true; } else if (!target.isEmpty() && target.equals( category)) { hit = true; } if (hit && !matched .contains(cond)) { matched.add(cond); } } } } return matched; } /** * 从条件文本中提取目标群系分类 * "打卡上报地狱任意群系" → "下界" * "打卡任意海洋群系" → "海洋" * "打卡任意群系" → "ALL" * "打卡任意风景点" → "" */ private String extractBiome(String text) { if (text == null) return ""; // 逐个匹配分类关键词 for (Map.Entry en : CATEGORY_KEYWORDS.entrySet()) { if (text.contains(en.getKey())) { return en.getValue(); } } if (text.contains("风景点") || text.contains("风景")) { return "ALL"; } // 有"群系"但无具体分类 → ALL if (text.contains("群系")) { return "ALL"; } return ""; } // ========== 标记条件 ========== public void markConditionComplete( String player, String conditionName) { if (player == null || conditionName == null) return; storage.setCondition( player, conditionName); } // ========== 事件回调 ========== public void onBlockMined( String player, String material) { String key = "MINE:" + material; setCounter(player, key, getCounter(player, key) + 1); // 总计数 setCounter(player, "MINE:ALL", getCounter(player, "MINE:ALL") + 1); } public boolean onReportVillage( String player, String worldName, double x, double y, double z) { String dbKey = "village_lastPos"; String curPos = worldName + "|" + String.format( "%.0f", x / 32) + "|" + String.format( "%.0f", y / 32) + "|" + String.format( "%.0f", z / 32); Object lastObj = storage.getCondition( player, dbKey); String lastPos = lastObj != null ? lastObj.toString() : ""; if (curPos.equals(lastPos)) { return false; } storage.setConditionRaw( player, dbKey, curPos); String key = "REPORT_VILLAGE"; int cur = getCounter(player, key); setCounter(player, key, cur + 1); return true; } public void onMobKill( String player, String mobType) { String key = "KILL:" + mobType; int cur = getCounter(player, key); setCounter(player, key, cur + 1); } public void onPlayerKill( String player) { String key = "KILL_PLAYER"; int cur = getCounter(player, key); setCounter(player, key, cur + 1); } public void onItemCrafted( String player) { String key = "CRAFT"; int cur = getCounter(player, key); setCounter(player, key, cur + 1); } public void onFishCaught( String player) { String key = "FISH"; int cur = getCounter(player, key); setCounter(player, key, cur + 1); } public void onPlayTime( String player, long ms) { String key = "PLAYTIME"; long cur = getCounterLong( player, key); setCounter(player, key, cur + ms); } // ========== 内部:条件判定 ========== private boolean isStageComplete( String player, List conditions) { if (conditions == null || conditions.isEmpty()) return true; for (String cond : conditions) { if (isOptional(cond)) continue; if (!isConditionMet( player, cond)) return false; } return true; } public boolean isConditionMet( String player, String rawCond) { if (player == null || rawCond == null) return false; String key = stripOptional(rawCond); // 打卡群系 if (key.contains("打卡") && (key.contains("群系") || key.contains("风景点"))) { String biome = extractBiome(key); if ("ALL".equals(biome)) { // 打卡任意群系 int total = getCounter( player, "biome_ALL"); return total >= 1; } if (!biome.isEmpty()) { // 打卡特定分类群系 int catCount = getCounter( player, "biomecat_" + biome); } // 无特定要求 return getCounter( player, "biome_ALL") >= 1; } // 阶段依赖 if (key.contains("完成") && key.contains("阶段")) { return checkStageDependency( player, key); } // 注册登录 if ("完成注册登录".equals(key)) { return getDb() != null && getDb().userExists( player); } // 绑定邮箱 if (key.contains("绑定Email") || key.contains("绑定邮箱")) { Object val = getDb().getField( player, "email"); return val != null && !val.toString().isEmpty(); } // 在线时间 if (key.contains("在线") && key.contains("小时")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; long cur = getCounterLong( player, "PLAYTIME"); return cur >= needed * 3600000L; } // 打招呼/发消息/聊天 if (key.contains("打招呼") || key.contains("打个招呼") || key.contains("发消息") || key.contains("聊天")) { return getCounter( player, "CHAT") >= 1; } // 破坏/放置方块 if ((key.contains("破坏") || key.contains("放置")) && key.contains("方块")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; int total = getCounter( player, "MINE:ALL") + getCounter( player, "PLACE:ALL"); return total >= needed; } // 连续登录 if (key.contains("连续") && key.contains("登录")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; Object val = getDb().getField( player, "checkin_streak"); int streak = val instanceof Number ? ((Number) val).intValue() : 0; return streak >= needed; } // 挖矿 if (key.startsWith("MINE:")) { String[] p = key.split(":"); if (p.length >= 3) { return getCounter(player, "MINE:" + p[1]) >= parseInt(p[2]); } } // 击杀生物 if (key.startsWith("KILL:")) { String[] p = key.split(":"); if (p.length >= 3) { return getCounter(player, "KILL:" + p[1]) >= parseInt(p[2]); } } // 击杀玩家 if (key.startsWith("KILL_PLAYER")) { String[] p = key.split(":"); int n = p.length >= 2 ? parseInt(p[1]) : 1; return getCounter(player, "KILL_PLAYER") >= n; } // 合成 if (key.startsWith("CRAFT")) { String[] p = key.split(":"); int n = p.length >= 2 ? parseInt(p[1]) : 1; return getCounter( player, "CRAFT") >= n; } // 钓鱼 if (key.startsWith("FISH")) { String[] p = key.split(":"); int n = p.length >= 2 ? parseInt(p[1]) : 1; return getCounter( player, "FISH") >= n; } // 在线时长 if (key.startsWith("PLAYTIME")) { String[] p = key.split(":"); long n = p.length >= 2 ? Long.parseLong(p[1]) : 0; return getCounterLong(player, "PLAYTIME") >= n; } // 上报村庄 if (key.contains("上报") && key.contains("村庄")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; int cur = getCounter( player, "REPORT_VILLAGE"); return cur >= needed; } // 探索村庄 if (key.contains("探索") && key.contains("村庄")) { return getCounter( player, "REPORT_VILLAGE") >= 1; } // 近7天登录记录 if (key.contains("7天") || key.contains("七天")) { int needed = parseNumberFromText(key); if (needed <= 0) needed = 1; // 诊断:读取数据库所有字段 try { var user = getDb().getUser( player); if (user != null) { plugin.getLogger().info( "[QuestTracker] " + "登录诊断 " + player + ": " + user.toString()); } else { plugin.getLogger().info( "[QuestTracker] " + "登录诊断: " + player + " getUser返回null"); } } catch (Exception e) { plugin.getLogger().info( "[QuestTracker] " + "登录诊断异常: " + e.getMessage()); } int streak = getIntField( player, "checkin_streak"); if (streak == 0) { streak = getIntField( player, "streak"); } if (streak == 0) { streak = getIntField( player, "total_checkin_days"); } plugin.getLogger().info( "[QuestTracker] 登录检测: " + player + " 需要=" + needed + " 实际=" + streak); return streak >= needed; } // 完成新手任务全部内容 if (key.contains("新手任务") && key.contains("全部")) { List quests = getQuests("新人任务"); for (QuestFile qf : quests) { if (!isStageComplete( player, qf.conditions)) return false; } return true; } // 完成主线任务全部内容 if (key.contains("主线任务") && key.contains("全部")) { List quests = getQuests("主线任务"); for (QuestFile qf : quests) { if (!isStageComplete( player, qf.conditions)) return false; } return true; } return false; } public void debugLogin(String player) { try { var user = getDb().getUser(player); if (user != null) { plugin.getLogger().info( "[QuestTracker] " + "=== " + player + " 数据库全部字段 ==="); for (var entry : user.entrySet()) { plugin.getLogger().info( "[QuestTracker] " + entry.getKey() + " = " + entry.getValue() + " (" + (entry.getValue() != null ? entry.getValue() .getClass() .getSimpleName() : "null") + ")"); } } else { plugin.getLogger().info( "[QuestTracker] " + "getUser返回null: " + player); } } catch (Exception e) { plugin.getLogger().info( "[QuestTracker] " + "debug异常: " + e.getMessage()); } } // ========== 内部:计数器 ========== private int getCounter( String player, String key) { Object val = storage.getCondition( player, "cnt_" + key); if (val instanceof Number) { return ((Number) val).intValue(); } return 0; } private long getCounterLong( String player, String key) { Object val = storage.getCondition( player, "cnt_" + key); if (val instanceof Number) { return ((Number) val).longValue(); } return 0; } private void setCounter( String player, String key, int value) { storage.setConditionRaw( player, "cnt_" + key, String.valueOf(value)); } private void setCounter( String player, String key, long value) { storage.setConditionRaw( player, "cnt_" + key, String.valueOf(value)); } // ========== 工具 ========== private boolean toBool(Object val) { if (val == null) return false; if (val instanceof Boolean) return (Boolean) val; if (val instanceof Number) return ((Number) val) .intValue() != 0; String s = String.valueOf(val); return "1".equals(s) || "true" .equalsIgnoreCase(s); } public boolean isOptional(String cond) { if (cond == null) return false; String c = cond; return c.contains("(进阶)") || c.contains("(可选)") || c.contains(",可选)") || c.contains(",可选)") || c.contains("【可选】") || c.contains("【进阶】"); } private String stripOptional(String cond) { if (cond == null) return ""; return cond.replace("(进阶)", "") .replace("(可选)", "") .replace(",可选)", "") .replace(",可选)", "") .replace("【可选】", "") .replace("【进阶】", "") .trim(); } private int parseInt(String s) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return 0; } } private Object getDbField( String player, String field) { Main main = (Main) plugin; return main.getDb().getField( player, field); } /** * 从文本中提取第一个数字 * "在线时间≥1小时" → 1 * "破坏/放置方块100个" → 100 */ private int parseNumberFromText( String text) { java.util.regex.Matcher m = java.util.regex.Pattern .compile("(\\d+)") .matcher(text); if (m.find()) { try { return Integer.parseInt( m.group(1)); } catch (Exception e) { return 0; } } return 0; } public void onBlockPlaced( String player, String material) { String key = "PLACE:" + material; setCounter(player, key, getCounter(player, key) + 1); setCounter(player, "PLACE:ALL", getCounter(player, "PLACE:ALL") + 1); } public void onPlayerChat( String player) { String key = "CHAT"; setCounter(player, key, getCounter(player, key) + 1); } private String formatMs(long ms) { long sec = ms / 1000; long min = sec / 60; long hr = min / 60; if (hr > 0) return hr + "小时" + (min % 60) + "分"; if (min > 0) return min + "分" + (sec % 60) + "秒"; return sec + "秒"; } // ========== 数据结构 ========== public static class QuestFile { public String displayName = ""; public String fileName = ""; public List details = new ArrayList<>(); public List rewards = new ArrayList<>(); public List conditions = new ArrayList<>(); public boolean hasConditions() { return conditions != null && !conditions.isEmpty(); } } }