feat: 新增寻宝功能,修复题目重载后不生效的问题

This commit is contained in:
ypsdf1
2026-06-02 22:51:50 +08:00
parent c84b0709e8
commit 7d62fcfafc
12 changed files with 3115 additions and 382 deletions
+26 -87
View File
@@ -1,9 +1,6 @@
package Sdf1_game;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -11,6 +8,8 @@ import java.util.List;
public class FileGenerator {
public static void ensureAll(File base, File qDir) {
qDir.mkdirs();
writeIfAbsent(
new File(base, "配置.txt"),
defaultConfig());
@@ -23,17 +22,17 @@ public class FileGenerator {
writeIfAbsent(
new File(qDir, "问答题.md"),
defaultOpen());
}
}
private static void writeIfAbsent(File f,
List<String> lines) {
if (f.exists()) return;
try {
f.getParentFile().mkdirs();
OutputStreamWriter w = new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8);
OutputStreamWriter w =
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8);
for (String line : lines) {
w.write(line);
w.write("\n");
@@ -43,118 +42,58 @@ public class FileGenerator {
}
}
// ========== 快问快答配置 ==========
private static List<String> defaultConfig() {
List<String> l = new ArrayList<>();
l.add("# ===== 快问快答 主配置 =====");
l.add("# 支持 # // /* */ <!-- --> 注释");
l.add("");
l.add("# 运行模式: 自动 / 手动");
l.add("# 模式区别:");
l.add("# 手动模式,纯手动出题");
l.add("# 自动模式:手自一体化,按下面配置规则自动出题");
l.add("模式: 自动");
l.add("");
l.add("# 最少在线人数");
l.add("最少在线人数: 3");
l.add("");
l.add("# 出题间隔");
l.add("# 支持: 秒(s) 分钟(m) 小时(h)");
l.add("# 出题间隔(支持中文单位)");
l.add("最小间隔: 5分钟");
l.add("最大间隔: 15分钟");
l.add("");
l.add("# 答题时间");
l.add("答题时间: 2分钟");
l.add("");
l.add("# 蒙题防护");
l.add("# 选择题中,2秒内错误答案超过此数则罚时");
l.add("# 罚时期间答案无效");
l.add("蒙题检测数: 2");
l.add("");
l.add("# 罚时时间(秒)");
l.add("# 支持: 秒(s) 分钟(m) 小时(h)");
l.add("罚时时间: 10秒");
l.add("");
l.add("罚时开始提示: §c你回答太快了,请冷静!");
l.add("罚时结束提示: §a罚时已结束");
l.add("# 消息前缀");
l.add("消息前缀: §e§l【快问快答】§r");
l.add("");
l.add("# 蒙题防护提示");
l.add("罚时开始提示: §c你回答太快了,请冷静10秒!");
l.add("罚时结束提示: §a罚时已结束,你可以继续答题了");
return l;
}
private static List<String> defaultChoice() {
List<String> l = new ArrayList<>();
l.add("# ===== 选择题 =====");
l.add("# 格式: | 问题 | 选项A | 选项B | [选项C] | [选项D] | 答案 | 赏金 |");
l.add("# 空选项列自动跳过");
l.add("");
l.add("| 问题 | 选项A | 选项B | 选项C | 选项D | 答案 | 赏金 |");
l.add("|------|-------|-------|-------|-------|------|------|");
l.add("| 钻石矿在几层以下生成? | 4层 | 16层 | 32层 | 64层 | B | 10 |");
l.add("| 末影龙的血量是多少? | 100 | 200 | 300 | 500 | B | 15 |");
l.add("| 下界合金锭需要几个碎片? | 2个 | 3个 | 4个 | 5个 | C | 10 |");
l.add("| 铁砧最多修复几次后损坏? | 3次 | 5次 | 6次 | 10次 | C | 10 |");
l.add("| 下界合金剑的攻击力? | 6 | 7 | 8 | 9 | D | 12 |");
l.add("| 哪种生物不在平原生成? | 苦力怕 | 僵尸 | 末影人 | 骷髅 | C | 8 |");
l.add("| 信标最少几层方块? | 1层 | 2层 | 3层 | 4层 | C | 10 |");
l.add("| 附魔台最多放几本书? | 4本 | 8本 | 12本 | 15本 | D | 10 |");
l.add("| 熔岩在几格内造成伤害? | 1格 | 2格 | 3格 | | A | 8 |");
l.add("| 末影珍珠冷却时间? | 0秒 | 1秒 | 5秒 | | B | 10 |");
l.add("# ===== 选择题/多选题 =====");
l.add("# 答案含逗号=多选,全选=全选");
l.add("| 问题 | A | B | C | D | 答案 | 赏金 |");
l.add("|------|---|---|---|---|------|------|");
l.add("| 钻石用什么镐挖? | 木镐 | 石镐 | 铁镐 | 钻石镐 | D | 10 |");
l.add("| 末影龙在哪? | 下界 | 主世界 | 末地 | 末地城 | C | 10 |");
l.add("| 以下哪些是负面效果? | 速度 | 中毒 | 夜视 | 力量 | B | 10 |");
return l;
}
private static List<String> defaultFill() {
List<String> l = new ArrayList<>();
l.add("# ===== 填空题 =====");
l.add("# 编号. 问题(赏金1)(赏金2)...");
l.add("# 答案:答案1,答案2,...");
l.add("");
l.add("1. 钻石矿在( )层以下生成。(10)");
l.add("答案:16");
l.add("");
l.add("2. 末影龙被击败后掉落( )经验。(15)");
l.add("答案:12000");
l.add("");
l.add("3. ( )是游戏中最坚硬的方块。(15)");
l.add("答案:基岩");
l.add("");
l.add("4. 下界合金锭由( )个碎片和( )个金锭合成。(10)(10)");
l.add("答案:4,4");
l.add("");
l.add("5. MC一共有( )个维度。(5)");
l.add("答案:3");
l.add("");
l.add("6. 信标最少需要( )层方块才能激活。(10)");
l.add("答案:3");
l.add("");
l.add("7. 钻石剑攻击力( )点,下界合金剑( )点。(8)(8)");
l.add("答案:7,8");
l.add("");
l.add("8. 饱和效果的英文ID是( )。(5)");
l.add("答案:saturation");
l.add("");
l.add("9. 每个铁砧使用后消耗( )点耐久。(5)");
l.add("答案:1");
l.add("");
l.add("10. 凋零Boss被击败后掉落( )个下界之星。(10)");
l.add("答案:1");
l.add("# (空) 表示填空位");
l.add("1. 钻石矿在Y=(空)以下生成(20)");
l.add("答案: 16");
return l;
}
private static List<String> defaultOpen() {
List<String> l = new ArrayList<>();
l.add("# ===== 问答题 =====");
l.add("# | 问题 | 答案 | 赏金 |");
l.add("");
l.add("| 问题 | 答案 | 赏金 |");
l.add("|------|------|------|");
l.add("| MC的全称是什么? | Minecraft | 20 |");
l.add("| Minecraft的创始人是谁? | Notch | 20 |");
l.add("| 下界合金锭的英文名? | Netherite Ingot | 15 |");
l.add("| 末影螨的英文名? | Endermite | 12 |");
l.add("| 信标的英文名? | Beacon | 10 |");
l.add("| M.C的全称是什么? | Mojang Studios | 10 |");
return l;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -23,6 +23,14 @@ public class QuestionLoader {
File file = new File(
plugin.getDataFolder(), resourcePath);
plugin.getLogger().info(
"[快问快答] 加载文件: "
+ file.getAbsolutePath()
+ " 存在=" + file.exists()
+ " 大小=" + (file.exists()
? file.length() + "字节"
: "N/A"));
if (!file.exists()) {
plugin.getLogger().warning(
"[快问快答] 文件不存在: "
@@ -46,6 +54,21 @@ public class QuestionLoader {
List<String> cleaned = cleanComments(br);
plugin.getLogger().info(
"[快问快答] 有效行数: "
+ cleaned.size());
if (!cleaned.isEmpty()) {
plugin.getLogger().info(
"[快问快答] 前3行: "
+ cleaned.get(0)
+ (cleaned.size() > 1
? " | " + cleaned.get(1)
: "")
+ (cleaned.size() > 2
? " | " + cleaned.get(2)
: ""));
}
switch (type) {
case CHOICE:
parseChoice(cleaned, questions);
@@ -58,6 +81,11 @@ public class QuestionLoader {
break;
}
plugin.getLogger().info(
"[快问快答] 解析出: "
+ questions.size() + "题"
+ " (" + resourcePath + ")");
} catch (IOException e) {
plugin.getLogger().warning(
"[快问快答] 加载失败: "
+555 -62
View File
@@ -8,7 +8,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.command.CommandSender;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
@@ -64,6 +64,12 @@ public class QuizManager implements Listener {
"§c你回答太快了,请冷静10秒!";
private String punishEndMsg =
"§a罚时已结束,你可以继续答题了";
// 填空题:记录每个玩家的答案
private final Map<UUID, String> fillAnswers
= new ConcurrentHashMap<>();
private final Map<UUID, Set<String>> multiAnswers
= new ConcurrentHashMap<>();
public QuizManager(Main plugin, BondBridge bb) {
@@ -117,6 +123,69 @@ public class QuizManager implements Listener {
}
}
/** 填空题多空:抢答,全空答对才提交 */
private void handleFillAnswer(
AsyncPlayerChatEvent event,
String msg,
Question q,
Player player,
UUID uid) {
event.setCancelled(true);
if (answered.contains(uid)) return;
// 罚时期间忽略
if (isPunished(uid)) return;
// 解析玩家答案
String[] playerParts = msg
.replaceAll("[,,\\s]+", ",")
.split(",");
String[] correctParts =
q.getAnswer().split("[,,]");
// 逐空比对
int correctCount = 0;
for (int i = 0;
i < correctParts.length
&& i < playerParts.length; i++) {
if (correctParts[i].trim().toLowerCase()
.equals(playerParts[i].trim()
.toLowerCase())) {
correctCount++;
}
}
// 全部答对 → 获胜
if (correctCount >= correctParts.length) {
answered.add(uid);
wrongTimes.remove(uid);
fillAnswers.remove(uid);
int reward;
if (q.getRewards().length > 1) {
reward = q.getRewardForCount(
correctCount);
} else {
reward = q.getRewards()[0];
}
broadcastAnswer(player, q, reward,
"填空题");
return;
}
// ★ 答错或部分答对 → 记录,等超时结算
fillAnswers.put(uid, msg.trim());
// 蒙题防护:纯数字短答案检测
if (msg.matches("[\\d,,\\s]+")) {
recordWrong(uid);
if (shouldPunish(uid)) {
applyPunish(uid, player);
}
}
}
// ========== 读配置 ==========
@@ -196,20 +265,84 @@ public class QuizManager implements Listener {
public void loadQuestions() {
loadConfig();
choiceQ = QuestionLoader.load(plugin,
"快问快答/题目/选择题.md");
multiQ = QuestionLoader.load(plugin,
"快问快答/题目/多选题.md");
fillQ = QuestionLoader.load(plugin,
"快问快答/题目/填空题.md");
openQ = QuestionLoader.load(plugin,
"快问快答/题目/问答题.md");
int oldChoice = choiceQ.size();
int oldFill = fillQ.size();
int oldOpen = openQ.size();
int oldMulti = multiQ.size();
// ★ 每次重新创建列表
choiceQ = new ArrayList<>(
QuestionLoader.load(plugin,
"快问快答/题目/选择题.md"));
multiQ = new ArrayList<>(
QuestionLoader.load(plugin,
"快问快答/题目/多选题.md"));
fillQ = new ArrayList<>(
QuestionLoader.load(plugin,
"快问快答/题目/填空题.md"));
openQ = new ArrayList<>(
QuestionLoader.load(plugin,
"快问快答/题目/问答题.md"));
plugin.getLogger().info(
"[快问快答] 单选" + choiceQ.size()
+ " 多选" + multiQ.size()
+ " 填空" + fillQ.size()
+ " 问答" + openQ.size());
"[快问快答] ★题目重载完成:");
plugin.getLogger().info(
"[快问快答] 选择题: "
+ oldChoice + " → "
+ choiceQ.size());
plugin.getLogger().info(
"[快问快答] 多选题: "
+ oldMulti + " → "
+ multiQ.size());
plugin.getLogger().info(
"[快问快答] 填空题: "
+ oldFill + " → "
+ fillQ.size());
plugin.getLogger().info(
"[快问快答] 问答题: "
+ oldOpen + " → "
+ openQ.size());
}
public void reload() {
stopAuto();
if (countTask != null) {
countTask.cancel();
countTask = null;
}
loadQuestions();
firstRun = true;
lastQuizTime = System.currentTimeMillis();
startAuto();
plugin.getLogger().info(
"[快问快答] ★已重载并重启自动出题");
plugin.getLogger().info("\n" +
" __ __ _ _ \n" +
" \\ \\ / / | | | | \n" +
" \\ \\ /\\ / /__| | ___ ___ _ __ ___ ___ | |_ ___ \n" +
" \\ \\/ \\/ / _ \\ |/ __/ _ \\| '_ ` _ \\ / _ \\ | __/ _ \\ \n" +
" \\ /\\ / __/ | (_| (_) | | | | | | __/ | || (_) | \n" +
" \\/ \\/ \\___|_|\\___\\___/|_| |_| |_|\\___| \\__\\___/ _ \n" +
" | | (_) \n" +
" ___ __ _ ___ _ _ _ _ __ _ _ __ | |_ __ _ _ __ __ ___ __ _ _ __ ___ ___ _ ____ _____ _ __ \n" +
" / __/ _` |/ _ \\ | | | | | | |/ _` | '_ \\ | __/ _` | '_ \\ \\ \\/ / |/ _` | '_ \\ / __|/ _ \\ '__\\ \\ / / _ \\ '__| \n" +
" | (_| (_| | (_) | | |_| | |_| | (_| | | | | | || (_| | | | | > <| | (_| | | | | \\__ \\ __/ | \\ V / __/ | \n" +
" \\___\\__,_|\\___/ \\__, |\\__,_|\\__,_|_| |_| \\__\\__,_|_| |_|_/_/\\_\\_|\\__,_|_| |_| |___/\\___|_|_ _\\_/ \\___|_| \n" +
" __/ | (_) _ |__ \\ | | (_) | (_)/ _| \n" +
" ___ ___ _ ____ |___/ _ __ _ _ __(_) _ __ ___ ___ ) | _ _ _ __ ___| |__ _ __| |_| |_ _ _ ___ _ __ \n" +
" / __|/ _ \\ '__\\ \\ / / _ \\ '__| | | '_ \\ | '_ ` _ \\ / __| / / | | | | '_ \\/ __| '_ \\| |/ _` | | _| | | | / __| '_ \\ \n" +
" \\__ \\ __/ | \\ V / __/ | | | |_) | | | | | | | (__ / /_ | |_| | |_) \\__ \\ | | | | (_| | | | | |_| || (__| | | |\n" +
" |___/\\___|_| \\_/ \\___|_| |_| .__(_) |_| |_| |_|\\___|____(_)__, | .__/|___/_| |_|_|\\__,_|_|_| \\__,_(_)___|_| |_|\n" +
" | | __/ | | \n" +
" _ ____ _|_| ________ ___ |___/|_| \n" +
" | | _ |___ \\ / _ \\ / /____ / _ \\ \n" +
" _ __ ___ ___| |_(_) __) | | | |/ /_ / / (_) | \n" +
" | '_ \\ / _ \\/ __| __| |__ <| | | | '_ \\ / / \\__, | \n" +
" | |_) | (_) \\__ \\ |_ _ ___) | |_| | (_) / / / / \n" +
" | .__/ \\___/|___/\\__(_) |____/ \\___/ \\___/_/ /_/ \n" +
" | | \n" +
" |_| ");
}
public int getMultiCount() {
@@ -477,6 +610,7 @@ public class QuizManager implements Listener {
}
msg.append("\n§7§o多个答案用逗号分隔"
+ ",如: A,B");
// 显示赏金明细
int[] rw = q.getRewards();
if (rw.length > 1) {
@@ -565,7 +699,6 @@ public class QuizManager implements Listener {
}
// ========== 结束 ==========
private void endQuiz(boolean hadWinner) {
Question snapshot;
synchronized (lock) {
@@ -577,23 +710,296 @@ public class QuizManager implements Listener {
countTask.cancel();
countTask = null;
}
if (!hadWinner) {
Bukkit.broadcastMessage(prefix
+ "§72分钟内无人答对,本题跳过~");
if (snapshot != null) {
Bukkit.broadcastMessage(prefix
+ "§e正确答案: §a"
+ snapshot.getAnswer());
// ★ 填空题超时结算
if (snapshot.getType()
== Question.Type.MULTI
&& !multiAnswers.isEmpty()) {
Set<String> correctSet =
new HashSet<>();
for (String s : snapshot
.getAnswer().split(",")) {
correctSet.add(s.trim()
.toLowerCase());
}
int bestCount = 0;
UUID bestUid = null;
for (Map.Entry<UUID,
Set<String>> entry
: multiAnswers.entrySet()) {
int correct = 0;
for (String c : correctSet) {
if (entry.getValue()
.contains(c))
correct++;
}
if (correct > bestCount) {
bestCount = correct;
bestUid = entry.getKey();
}
}
if (bestUid == null) return;
boolean mustFull = snapshot
.getRewards().length == 1;
if (mustFull
&& bestCount < correctSet.size()) {
Bukkit.broadcastMessage(prefix
+ "§7本题要求全对,"
+ "无人全对,不发奖");
return;
}
// 只给最先到达的那一个
giveMultiTimeoutReward(
snapshot, bestUid,
bestCount);
}
String nextTime = getNextQuizTime();
Bukkit.broadcastMessage(prefix
+ "§7下次出题: §e"
+ nextTime);
}
}
multiAnswers.clear();
fillAnswers.clear();
}
private void giveMultiTimeoutReward(
Question q, UUID pUid, int correctCount) {
Player p = Bukkit.getPlayer(pUid);
if (p == null || !p.isOnline()) return;
int reward = q.getRewards().length > 1
? q.getRewardForCount(correctCount)
: q.getRewards()[0];
boolean bondOk = false;
if (bondBridge.isHooked()) {
bondOk = bondBridge.addBonds(
p.getName(), reward,
"多选题超时" + correctCount + "对");
}
Bukkit.broadcastMessage(prefix
+ "§a" + p.getName()
+ " §7答对了 §e" + correctCount
+ "§7 个选项,获得 §6" + reward
+ " §e债券"
+ (bondOk ? "" : "(债券未连接)"));
}
/** 填空题超时:只给最先到达且答对最多的人 */
private void settleFillTimeout(Question q) {
String[] correctParts =
q.getAnswer().split("[,,]");
int totalBlanks = correctParts.length;
// 统计每个人答对几个空
int bestCount = 0;
UUID bestUid = null;
for (Map.Entry<UUID, String> entry
: fillAnswers.entrySet()) {
int correct = countFillCorrect(
entry.getValue(), correctParts);
if (correct > bestCount) {
bestCount = correct;
bestUid = entry.getKey();
}
}
if (bestUid == null) return;
boolean mustFull =
q.getRewards().length == 1;
if (mustFull && bestCount < totalBlanks) {
Bukkit.broadcastMessage(prefix
+ "§7本题要求全对,无人全对,不发奖");
return;
}
giveFillReward(q, bestUid, bestCount);
}
private int countFillCorrect(String playerAns,
String[] correct) {
String[] pp = playerAns
.replaceAll("[,,\\s]+", ",")
.split(",");
int c = 0;
for (int i = 0;
i < correct.length
&& i < pp.length; i++) {
if (correct[i].trim().toLowerCase()
.equals(pp[i].trim()
.toLowerCase())) {
c++;
}
}
return c;
}
private void giveFillReward(Question q,
UUID pUid,
int correctCount) {
Player p = Bukkit.getPlayer(pUid);
if (p == null || !p.isOnline()) return;
int reward;
if (q.getRewards().length > 1) {
reward = q.getRewardForCount(
correctCount);
} else {
reward = q.getRewards()[0];
}
boolean bondOk = false;
if (bondBridge.isHooked()) {
bondOk = bondBridge.addBonds(
p.getName(), reward,
"填空题" + correctCount + "空对");
}
Bukkit.broadcastMessage(prefix
+ "§a" + p.getName()
+ " §7答对了 §e" + correctCount
+ "/" + q.getBlankCount()
+ "§7 空,获得 §6" + reward
+ " §e债券"
+ (bondOk ? "" : "(债券未连接)"));
}
/** 多选题发奖 */
private void giveMultiReward(Question q,
UUID pUid,
int correctCount) {
Player p = Bukkit.getPlayer(pUid);
if (p == null || !p.isOnline()) return;
int reward =
q.getRewardForCount(correctCount);
boolean bondOk = false;
if (bondBridge.isHooked()) {
bondOk = bondBridge.addBonds(
p.getName(), reward,
"多选题" + correctCount + "对");
}
Bukkit.broadcastMessage(prefix
+ "§a" + p.getName()
+ " §7答对了 §e" + correctCount
+ "§7 个选项,获得 §6" + reward
+ " §e债券"
+ (bondOk ? "" : "(债券未连接)"));
}
/** 多选题:抢答,第一个全对的获胜 */
private void handleMultiAnswer(
AsyncPlayerChatEvent event,
String msg,
Question q,
Player player,
UUID uid) {
event.setCancelled(true);
if (answered.contains(uid)) return;
Set<String> correctSet = new HashSet<>();
for (String s : q.getAnswer().split(",")) {
correctSet.add(s.trim().toLowerCase());
}
Set<String> playerSet = new HashSet<>();
for (String s :
msg.split("[,,\\s]+")) {
String clean = s.trim().toLowerCase()
.replaceAll(
"[()().,。,]", "");
if (!clean.isEmpty()) {
playerSet.add(clean);
}
}
// 是否全对
boolean fullMatch = correctSet
.equals(playerSet);
if (!fullMatch) {
// 没全对 → 提示,继续抢答
int correct = 0;
for (String c : correctSet) {
if (playerSet.contains(c)) correct++;
}
if (correct > 0) {
player.sendMessage("§e答对了 §a"
+ correct + "/"
+ correctSet.size()
+ "§e 个,继续抢答!");
} else {
player.sendMessage(
"§7答错了,再试试!");
}
return;
}
// ★ 全对 → 第一个答对的获胜
answered.add(uid);
int reward = q.getRewards().length > 1
? q.getRewardForCount(
correctSet.size())
: q.getRewards()[0];
boolean bondOk = false;
if (bondBridge.isHooked()) {
bondOk = bondBridge.addBonds(
player.getName(), reward,
"多选题答对");
}
String info = bondOk
? "§6" + reward + " §e债券"
: "§7(债券未连接)";
Bukkit.broadcastMessage(prefix
+ "§a" + player.getName()
+ " §7抢答成功§e[多选题]§7!");
Bukkit.broadcastMessage(prefix
+ "§e正确答案: §a"
+ q.getAnswer());
Bukkit.broadcastMessage(prefix
+ "§e赏金: " + info);
Bukkit.broadcastMessage(prefix
+ "§7下次出题: §e"
+ getNextQuizTime());
endQuiz(true);
}
// ========== 聊天监听 ==========
@EventHandler(priority = EventPriority.HIGHEST,
ignoreCancelled = true)
public void onChat(AsyncPlayerChatEvent event) {
@@ -610,86 +1016,140 @@ public class QuizManager implements Listener {
String msg = event.getMessage().trim();
if (msg.isEmpty()) return;
// ★ 蒙题防护:罚时期间直接跳过
if (isPunished(uid)) {
player.sendMessage(punishStartMsg);
if (isPunished(uid)) return;
// ★ 多选题:只处理全对
if (q.getType() == Question.Type.MULTI) {
Set<String> correctSet = new HashSet<>();
for (String s : q.getAnswer().split(",")) {
correctSet.add(s.trim().toLowerCase());
}
Set<String> playerSet = new HashSet<>();
for (String s : msg.split("[,,\\s]+")) {
String clean = s.trim().toLowerCase()
.replaceAll("[()().,。,]", "");
if (!clean.isEmpty()) {
playerSet.add(clean);
}
}
if (!correctSet.equals(playerSet)) return;
event.setCancelled(true);
answered.add(uid);
wrongTimes.remove(uid);
int reward = q.getRewards().length > 1
? q.getRewardForCount(correctSet.size())
: q.getRewards()[0];
broadcastAnswer(player, q, reward,
"多选题");
return;
}
// 只在选择题/多选题时检测蒙题
if (q.getType() == Question.Type.CHOICE
|| q.getType() == Question.Type.MULTI) {
// ★ 多空填空题:只处理全对
if (q.getType() == Question.Type.FILL
&& q.getBlankCount() > 1) {
boolean correct = isAnswerCorrect(msg, q);
String[] correctParts =
q.getAnswer().split("[,,]");
String[] playerParts = msg
.replaceAll("[,,\\s]+", ",")
.split(",");
if (!correct) {
// 记录错误时间
int correctCount = 0;
for (int i = 0;
i < correctParts.length
&& i < playerParts.length; i++) {
if (correctParts[i].trim().toLowerCase()
.equals(playerParts[i].trim()
.toLowerCase())) {
correctCount++;
}
}
// ★ 全空答对才提交
if (correctCount >= correctParts.length) {
event.setCancelled(true);
answered.add(uid);
wrongTimes.remove(uid);
fillAnswers.remove(uid);
int reward = q.getRewards().length > 1
? q.getRewardForCount(correctCount)
: q.getRewards()[0];
broadcastAnswer(player, q, reward,
"填空题");
return;
}
// 部分答对 → 记录不提交
fillAnswers.put(uid, msg.trim());
// 蒙题防护
if (msg.matches("[\\d,,\\s]+")) {
recordWrong(uid);
// 检查是否触发罚时
if (shouldPunish(uid)) {
applyPunish(uid, player);
}
}
return;
}
// ★ 单选题 / 单空填空题 / 问答题
boolean ok = false;
int reward = 0;
String detail = "";
switch (q.getType()) {
case CHOICE:
if (checkChoice(msg, q)) {
ok = true;
reward = q.getRewardForCount(1);
detail = q.getAnswer();
}
break;
case MULTI:
int mc =
countMultiCorrect(msg, q);
if (mc > 0) {
ok = true;
reward =
q.getRewardForCount(mc);
detail = mc + "/"
+ q.getCorrectCount();
}
break;
case OPEN:
if (msg.equalsIgnoreCase(
q.getAnswer())) {
if (msg.equalsIgnoreCase(q.getAnswer())) {
ok = true;
reward = q.getRewardForCount(1);
detail = q.getAnswer();
}
break;
case FILL:
int[] r = checkFill(msg, q);
if (r[0] > 0) {
ok = true;
reward = r[1];
detail = r[0] + "/"
+ q.getBlankCount();
}
break;
}
if (!ok) return;
if (!ok) {
// 蒙题防护
if (q.getType() == Question.Type.CHOICE
|| q.getType() == Question.Type.FILL) {
recordWrong(uid);
if (shouldPunish(uid)) {
applyPunish(uid, player);
}
}
return;
}
event.setCancelled(true);
answered.add(uid);
wrongTimes.remove(uid);
broadcastAnswer(player, q, reward,
q.getTypeName());
}
/** 统一广播答案 */
private void broadcastAnswer(Player player,
Question q,
int reward,
String typeName) {
boolean bondOk = false;
if (bondBridge.isHooked()) {
String reason = q.getTypeName() + "答对"
+ (q.getType()
== Question.Type.FILL
? " " + detail : "");
bondOk = bondBridge.addBonds(
player.getName(), reward, reason);
player.getName(), reward,
typeName + "答对");
}
String info = bondOk
@@ -699,17 +1159,45 @@ public class QuizManager implements Listener {
Bukkit.broadcastMessage(prefix
+ "§a" + player.getName()
+ " §7答对了§e["
+ q.getTypeName() + "]§7!");
+ typeName + "]§7!");
Bukkit.broadcastMessage(prefix
+ "§e正确答案: §a" + q.getAnswer());
+ "§e正确答案: §a"
+ q.getAnswer());
Bukkit.broadcastMessage(prefix
+ "§e赏金: " + info);
Bukkit.broadcastMessage(prefix
+ "§7下次出题: §e"
+ getNextQuizTime());
endQuiz(true);
}
/** 跳过当前题目 */
public void skipQuestion(CommandSender sender) {
if (!sender.hasPermission("sdf1_quiz.admin")) {
sender.sendMessage("§c无权限");
return;
}
if (!active) {
sender.sendMessage("§c当前没有答题");
return;
}
active = false;
cur = null;
if (countTask != null) {
countTask.cancel();
countTask = null;
}
sender.sendMessage("§a已跳过当前题目");
String nextTime = getNextQuizTime();
Bukkit.broadcastMessage(prefix
+ "§7本轮答题已跳过");
Bukkit.broadcastMessage(prefix
+ "§7下次出题: §e" + nextTime);
}
// ========== 蒙题防护 ==========
@@ -902,13 +1390,6 @@ public class QuizManager implements Listener {
return openQ.size();
}
public void reload() {
stopAuto();
loadQuestions();
firstRun = true;
lastQuizTime = System.currentTimeMillis();
startAuto();
}
/**
@@ -923,6 +1404,18 @@ public class QuizManager implements Listener {
broadcastQuestion();
}
}
/** 指定题型随机出题 */
public void forceQuestionByType(String type) {
if (Bukkit.getOnlinePlayers().size() < 1) {
return;
}
if (!active) {
lastQuizTime =
System.currentTimeMillis();
broadcastQuestion(type, 0);
// index=0 表示该题型内随机
}
}
/**
* 指定题型+编号(手动)
@@ -0,0 +1,37 @@
package Sdf1_game;
import java.util.ArrayList;
import java.util.List;
public class TreasureConfig {
public String name = "未命名";
public String world = "world";
public int minX, maxX;
public int minZ, maxZ;
public int minY = 64;
public int maxY = 80;
public int maxChests = 3;
public int spawnInterval = 300;
public int spawnChance = 100;
public boolean randomY = true;
public List<TreasureReward> rewards =
new ArrayList<>();
public TreasureConfig(String name) {
this.name = name;
}
public int getTotalWeight() {
int w = 0;
for (TreasureReward r : rewards) {
w += r.getWeight();
}
return w;
}
public String getBoundsStr() {
return "[" + minX + "," + minZ + "] ~ ["
+ maxX + "," + maxZ + "]";
}
}
@@ -0,0 +1,404 @@
package Sdf1_game;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class TreasureInventory {
// ★ 奖励索引标记(写在lore里用于精确匹配)
private static final String LORE_INDEX_PREFIX =
"§0§k";
public static class GuiHolder
implements InventoryHolder {
private final TreasureConfig config;
private final Player player;
private Inventory inv;
public GuiHolder(TreasureConfig config,
Player player) {
this.config = config;
this.player = player;
}
public TreasureConfig getConfig() {
return config;
}
public Player getPlayer() {
return player;
}
public void setInventory(Inventory inv) {
this.inv = inv;
}
@Override
public Inventory getInventory() {
return inv;
}
}
public static void open(Player player,
TreasureConfig config) {
GuiHolder holder =
new GuiHolder(config, player);
Inventory inv = Bukkit.createInventory(
holder, 27,
"§6§l寻宝宝箱 - " + config.name);
holder.setInventory(inv);
List<TreasureReward> selected =
rollRewards(config, 6);
int[] slots = {10, 12, 14, 16, 22, 24};
for (int i = 0;
i < selected.size()
&& i < slots.length; i++) {
TreasureReward r = selected.get(i);
inv.setItem(slots[i],
buildDisplayItem(r, i));
}
// 边框
ItemStack glass = new ItemStack(
Material.BLACK_STAINED_GLASS_PANE);
ItemMeta gm = glass.getItemMeta();
if (gm != null) {
gm.displayName(Component.text(" "));
glass.setItemMeta(gm);
}
for (int i = 0; i < 27; i++) {
if (inv.getItem(i) == null) {
inv.setItem(i, glass);
}
}
player.openInventory(inv);
}
/**
* 构建GUI显示物品(含索引标记用于匹配)
*/
private static ItemStack buildDisplayItem(
TreasureReward r, int index) {
switch (r.getType()) {
case BOND: {
ItemStack item = new ItemStack(
Material.GOLD_NUGGET, 1);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(
"§6" + r.getBondAmount()
+ " 债券"));
List<Component> lore =
new ArrayList<>();
lore.add(Component.text(
LORE_INDEX_PREFIX + index
+ ",BOND,"
+ r.getBondAmount()));
lore.add(Component.text(
"§7点击领取"));
meta.lore(lore);
item.setItemMeta(meta);
}
return item;
}
case COMMAND: {
ItemStack item = new ItemStack(
Material.PAPER, 1);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(
"§a命令奖励"));
List<Component> lore =
new ArrayList<>();
lore.add(Component.text(
LORE_INDEX_PREFIX + index
+ ",CMD"));
lore.add(Component.text(
"§7点击领取"));
meta.lore(lore);
item.setItemMeta(meta);
}
return item;
}
default: {
Material mat =
Material.matchMaterial(
r.getMaterialName());
if (mat == null) mat = Material.PAPER;
ItemStack item =
new ItemStack(mat, 1);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
// ★ 用中文名
String displayName = r
.getDisplayName();
if (displayName == null
|| displayName.isEmpty()
|| displayName.startsWith(
"§f")) {
displayName =
TreasureManager
.getItemDisplayName(
r.getMaterialName());
}
String amountStr = r.getAmount() > 1
? " x" + r.getAmount()
: "";
meta.displayName(Component.text(
displayName + amountStr));
List<Component> lore =
new ArrayList<>();
lore.add(Component.text(
LORE_INDEX_PREFIX + index
+ ",ITEM,"
+ r.getMaterialName()
+ ","
+ r.getAmount()));
lore.add(Component.text(
"§7点击领取"));
meta.lore(lore);
item.setItemMeta(meta);
}
return item;
}
}
}
// ===== 点击处理 =====
public static boolean handleClick(
InventoryClickEvent event,
BondBridge bb,
Main plugin) {
InventoryHolder holder =
event.getInventory().getHolder();
if (!(holder instanceof GuiHolder))
return false;
event.setCancelled(true);
if (event.getRawSlot() < 0
|| event.getRawSlot() >= 27)
return true;
ItemStack clicked = event.getCurrentItem();
if (clicked == null) return true;
if (clicked.getType()
== Material.BLACK_STAINED_GLASS_PANE)
return true;
GuiHolder gh = (GuiHolder) holder;
Player player = gh.getPlayer();
TreasureConfig config = gh.getConfig();
// ★ 从lore中解析索引标记
ItemMeta im = clicked.getItemMeta();
if (im == null || !im.hasLore()) return true;
int rewardIndex = -1;
for (Component lc : im.lore()) {
String line = net.kyori.adventure.text
.serializer.legacy
.LegacyComponentSerializer
.legacySection().serialize(lc);
if (line.startsWith(LORE_INDEX_PREFIX)) {
// 格式: §0§k0,BOND,50
String data = line.substring(
LORE_INDEX_PREFIX.length());
String[] parts = data.split(",");
try {
rewardIndex =
Integer.parseInt(parts[0]);
} catch (Exception ignored) {
}
break;
}
}
if (rewardIndex < 0
|| rewardIndex >= config.rewards.size())
return true;
TreasureReward matched =
config.rewards.get(rewardIndex);
giveReward(player, matched, bb, plugin);
// 领取后删除GUI物品
event.getInventory()
.setItem(event.getRawSlot(), null);
player.sendMessage("§a§l[寻宝]§r 领取了 §6"
+ matched.getDisplayName() + "§r!");
return true;
}
// ===== 发放奖励 =====
private static void giveReward(Player player,
TreasureReward r,
BondBridge bb,
Main plugin) {
switch (r.getType()) {
case BOND:
// ★ 直接发债券
if (bb != null && bb.isHooked()) {
bb.addBonds(player.getName(),
r.getBondAmount(),
"寻宝奖励");
}
player.sendMessage("§e获得了 §6"
+ r.getBondAmount()
+ " §e债券");
break;
case COMMAND:
String cmd = r.getCommand()
.replace("{player}",
player.getName());
Bukkit.dispatchCommand(
Bukkit.getConsoleSender(), cmd);
player.sendMessage(
"§e执行了命令奖励");
break;
case ITEM:
// ★ 给原版物品:不设自定义名称
Material mat =
Material.matchMaterial(
r.getMaterialName());
if (mat == null) mat = Material.PAPER;
ItemStack give =
new ItemStack(mat,
r.getAmount());
if (r.getDurationSec() > 0) {
// ★ 限时贴标物品
ItemMeta meta =
give.getItemMeta();
if (meta != null) {
meta.displayName(
Component.text(
"§b" + r.getDisplayName()));
List<Component> lore =
new ArrayList<>();
lore.add(Component.text(
"§7时限: "
+ TreasureReward
.formatTime(
r.getDurationSec())));
lore.add(Component.text(
"§c§o到期自动收回"));
meta.lore(lore);
TreasureReward.applyEnchant(
meta, r.getEnchant());
give.setItemMeta(meta);
}
player.getInventory()
.addItem(give);
// 定时收回
final ItemStack ref =
give.clone();
final int dur =
r.getDurationSec();
final String rName =
r.getDisplayName();
Bukkit.getScheduler()
.runTaskLater(plugin,
() -> {
player.getInventory()
.remove(ref);
if (player.isOnline()) {
player.sendMessage(
"§c[寻宝] " + rName
+ " §7已到期,自动收回");
}
},
dur * 20L);
} else {
// ★ 普通物品:纯原版,无自定义名称
player.getInventory()
.addItem(give);
}
player.sendMessage("§e获得了 §f"
+ mat.name()
+ " §ex" + r.getAmount());
break;
}
}
// ===== 权重随机 =====
private static List<TreasureReward> rollRewards(
TreasureConfig config, int count) {
List<TreasureReward> pool =
new ArrayList<>(config.rewards);
List<TreasureReward> result =
new ArrayList<>();
int total = config.getTotalWeight();
// ★ 日志
Bukkit.getLogger().info(
"[寻宝GUI] 抽奖: 奖励池="
+ pool.size()
+ " 总权重=" + total);
if (total <= 0 || pool.isEmpty()) {
Bukkit.getLogger().warning(
"[寻宝GUI] ★奖励池为空或总权重=0!");
return result;
}
for (int i = 0;
i < count && !pool.isEmpty(); i++) {
int roll = ThreadLocalRandom.current()
.nextInt(total);
int acc = 0;
TreasureReward picked = null;
for (TreasureReward r : pool) {
acc += r.getWeight();
if (roll < acc) {
picked = r;
break;
}
}
if (picked == null)
picked = pool.get(pool.size() - 1);
// ★ 日志
Bukkit.getLogger().info(
"[寻宝GUI] 抽中[" + i + "]: "
+ picked.getType()
+ " " + picked.getMaterialName()
+ " x" + picked.getAmount()
+ " 权重=" + picked.getWeight());
result.add(picked);
pool.remove(picked);
total = 0;
for (TreasureReward r : pool)
total += r.getWeight();
}
return result;
}
}
@@ -0,0 +1,180 @@
package Sdf1_game;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.util.Map;
public class TreasureListener implements Listener {
private final Main plugin;
private final TreasureManager manager;
public TreasureListener(Main plugin,
TreasureManager manager) {
this.plugin = plugin;
this.manager = manager;
}
// ===== 圈地工具选点 =====
@EventHandler
public void onToolClick(PlayerInteractEvent e) {
Player p = e.getPlayer();
// 只处理主手
if (e.getHand() != EquipmentSlot.HAND)
return;
if (!TreasureRegion.isTool(
p.getInventory().getItemInMainHand()))
return;
Block b = e.getClickedBlock();
if (b == null) return;
e.setCancelled(true);
Location loc = b.getLocation();
TreasureRegion tr =
TreasureRegion.get(p.getName());
if (e.getAction() == Action.LEFT_CLICK_BLOCK) {
// A点
tr.setPos1(loc);
int ex = tr.getExpand();
p.sendMessage("§a§l[圈地] §7A点已设置: §e"
+ loc.getBlockX() + ","
+ loc.getBlockY() + ","
+ loc.getBlockZ());
p.sendMessage("§7扩展范围: §e"
+ ex + "§7格");
p.sendMessage("§7输入 §e/寻宝 expand <数字> "
+ "§7调整范围");
p.sendMessage("§7输入 §e/寻宝 区域名 "
+ "§7命名并保存");
} else if (e.getAction()
== Action.RIGHT_CLICK_BLOCK) {
tr.setPos2(loc);
p.sendMessage("§a§l[圈地] §7B点已设置: §e"
+ loc.getBlockX() + ","
+ loc.getBlockY() + ","
+ loc.getBlockZ());
// ★ 设置B点后自动显示边框
tr.showBorder();
showPreview(p, tr);
}
}
// ===== 右键寻宝宝箱 =====
@EventHandler
public void onChestInteract(
PlayerInteractEvent e) {
if (e.getAction() != Action.RIGHT_CLICK_BLOCK)
return;
if (e.getHand() != EquipmentSlot.HAND) return;
Block b = e.getClickedBlock();
if (b == null || b.getType() != Material.CHEST)
return;
Location loc = b.getLocation();
if (!manager.isTreasure(loc)) return;
Player p = e.getPlayer();
e.setCancelled(true);
String regionName = null;
for (Map.Entry<Location, String> en
: manager.getActiveChests().entrySet()) {
if (en.getKey().equals(loc)) {
regionName = en.getValue();
break;
}
}
b.setType(Material.AIR);
manager.removeChest(loc);
if (regionName != null) {
TreasureConfig tc =
manager.getConfigs().get(regionName);
if (tc != null) {
TreasureInventory.open(p, tc);
}
}
}
// ===== 阻止破坏宝箱 =====
@EventHandler
public void onBreak(BlockBreakEvent e) {
if (e.getBlock().getType() != Material.CHEST)
return;
if (manager.isTreasure(
e.getBlock().getLocation())) {
e.setCancelled(true);
e.getPlayer().sendMessage(
"§c宝箱不能破坏");
}
}
// ===== GUI点击 =====
@EventHandler
public void onInventoryClick(
InventoryClickEvent e) {
if (e.getInventory().getHolder()
instanceof TreasureInventory.GuiHolder) {
TreasureInventory.handleClick(
e, plugin.getBondBridge(), plugin);
}
}
// ===== 预览区域范围 =====
private void showPreview(Player p,
TreasureRegion tr) {
Location a = tr.getPos1();
Location b = tr.getPos2();
if (a == null || b == null) {
p.sendMessage("§c请先设置A点");
return;
}
int ex = tr.getExpand();
int minX = Math.min(a.getBlockX(),
b.getBlockX()) - ex;
int maxX = Math.max(a.getBlockX(),
b.getBlockX()) + ex;
int minZ = Math.min(a.getBlockZ(),
b.getBlockZ()) - ex;
int maxZ = Math.max(a.getBlockZ(),
b.getBlockZ()) + ex;
p.sendMessage("§e§l[圈地] 区域预览:");
p.sendMessage("§7世界: §e"
+ a.getWorld().getName());
p.sendMessage("§7范围: §e["
+ minX + "," + minZ + "] ~ ["
+ maxX + "," + maxZ + "]");
p.sendMessage("§7大小: §e"
+ (maxX - minX + 1) + " x "
+ (maxZ - minZ + 1));
p.sendMessage("§7扩展: §e" + ex + "格");
p.sendMessage("§7输入 §e/寻宝 expand <数字> "
+ "§7调整");
p.sendMessage("§7输入 §e/寻宝 区域名 "
+ "§7保存");
}
}
@@ -0,0 +1,778 @@
package Sdf1_game;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public class TreasureManager {
private final Main plugin;
private final Map<String, TreasureConfig> configs
= new LinkedHashMap<>();
private final Map<Location, String> activeChests
= new LinkedHashMap<>();
public int minX, maxX;
public int minZ, maxZ;
public int minY = 64; // 最低高度
public int maxY = 80; // 最高高度
public TreasureManager(Main plugin) {
this.plugin = plugin;
}
// ========== 加载 ==========
public void loadAll() {
configs.clear();
File dir = new File(
plugin.getDataFolder(), "寻宝");
dir.mkdirs();
File[] files = dir.listFiles(
(d, n) -> n.endsWith(".txt")
&& !n.equals("配置说明.txt"));
if (files == null) return;
for (File f : files) {
try {
TreasureConfig tc = parseConfig(f);
if (tc != null) {
configs.put(tc.name, tc);
plugin.getLogger().info(
"[寻宝] ★加载: " + tc.name
+ " 奖励数量="
+ tc.rewards.size());
// ★ 打印每条奖励
for (int i = 0;
i < tc.rewards.size(); i++) {
TreasureReward r =
tc.rewards.get(i);
plugin.getLogger().info(
"[寻宝] 奖励["
+ i + "]: "
+ r.getType()
+ " "
+ r.getMaterialName()
+ " x"
+ r.getAmount()
+ " 权重="
+ r.getWeight()
+ " 名="
+ r.getDisplayName());
}
}
} catch (Exception e) {
plugin.getLogger().warning(
"[寻宝] 失败: " + f.getName()
+ " - " + e.getMessage());
e.printStackTrace();
}
}
plugin.getLogger().info(
"[寻宝] 共" + configs.size()
+ "个区域");
}
private TreasureConfig parseConfig(File f)
throws IOException {
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8));
TreasureConfig tc = new TreasureConfig(
f.getName().replace(".txt", ""));
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#")) continue;
int ci = line.indexOf(':');
if (ci < 0) continue;
String k = line.substring(0, ci).trim();
String v = line.substring(ci + 1).trim();
switch (k) {
case "区域名":
tc.name = v; break;
case "世界":
tc.world = v; break;
case "坐标":
// ★ 兼容中文逗号
String coordStr = v
.replace(",", ",");
String[] pp = coordStr.split(",");
if (pp.length == 4) {
int a = Integer.parseInt(
pp[0].trim());
int b = Integer.parseInt(
pp[1].trim());
int c = Integer.parseInt(
pp[2].trim());
int d = Integer.parseInt(
pp[3].trim());
tc.minX = Math.min(a, c);
tc.maxX = Math.max(a, c);
tc.minZ = Math.min(b, d);
tc.maxZ = Math.max(b, d);
}
break;
case "最多宝箱":
tc.maxChests =
Integer.parseInt(v);
break;
case "检查间隔":
tc.spawnInterval = parseTime(v);
break;
case "生成概率":
tc.spawnChance =
Integer.parseInt(v);
break;
case "随机高度":
tc.randomY = v.equals("是")
|| v.equals("true");
break;
case "奖励":
TreasureReward r =
parseReward(v);
if (r != null)
tc.rewards.add(r);
break;
case "高度":
// 格式: minY,maxY
String[] yp1 = v.split(",");
if (yp1.length == 2) {
int y1 = Integer.parseInt(
yp1[0].trim());
int y2 = Integer.parseInt(
yp1[1].trim());
tc.minY = Math.min(y1, y2);
tc.maxY = Math.max(y1, y2);
}
break;
}
}
br.close();
return tc;
}
private int parseTime(String s) {
s = s.trim().toLowerCase();
if (s.matches("\\d+"))
return Integer.parseInt(s);
if (s.endsWith("分钟") || s.endsWith("m")) {
return Integer.parseInt(
s.replaceAll("[^\\d]", "")) * 60;
}
if (s.endsWith("小时") || s.endsWith("h")) {
return Integer.parseInt(
s.replaceAll("[^\\d]", "")) * 3600;
}
if (s.endsWith("秒") || s.endsWith("s")) {
return Integer.parseInt(
s.replaceAll("[^\\d]", ""));
}
try { return Integer.parseInt(s); }
catch (Exception e) { return 300; }
}
// ★ 粒子边框任务
private BukkitTask borderTask;
private boolean borderShowing = false;
private String borderRegionName;
/** 显示已保存区域的粒子边框 */
public boolean showRegionBorder(String name) {
TreasureConfig tc = configs.get(name);
if (tc == null) return false;
World w = plugin.getServer()
.getWorld(tc.world);
if (w == null) return false;
hideRegionBorder();
normalize(tc);
borderShowing = true;
borderRegionName = name;
final int minX = tc.minX;
final int maxX = tc.maxX;
final int minZ = tc.minZ;
final int maxZ = tc.maxZ;
plugin.getLogger().info(
"[寻宝] 显示边框: " + name
+ " X=" + minX + "~" + maxX
+ " Z=" + minZ + "~" + maxZ);
borderTask = new BukkitRunnable() {
@Override
public void run() {
if (!borderShowing) return;
List<Location> points =
new ArrayList<>();
for (int x = minX; x <= maxX; x++) {
points.add(getP(w, x, minZ));
points.add(getP(w, x, maxZ));
}
for (int z = minZ + 1; z < maxZ; z++) {
points.add(getP(w, minX, z));
points.add(getP(w, maxX, z));
}
for (Player p : w.getPlayers()) {
int px = p.getLocation()
.getBlockX();
int pz = p.getLocation()
.getBlockZ();
if (px >= minX - 50
&& px <= maxX + 50
&& pz >= minZ - 50
&& pz <= maxZ + 50) {
for (Location loc : points) {
p.spawnParticle(
Particle.FLAME,
loc, 1, 0, 0, 0, 0);
}
}
}
}
}.runTaskTimer(plugin, 0L, 20L);
return true;
}
/** 隐藏边框 */
public boolean hideRegionBorder() {
if (borderTask != null) {
borderTask.cancel();
borderTask = null;
}
borderShowing = false;
borderRegionName = null;
return true;
}
/** 按名字隐藏(兼容旧调用) */
public boolean hideRegionBorder(String name) {
if (name == null
|| name.equals(borderRegionName)) {
return hideRegionBorder();
}
return false;
}
/** 检查边框是否在显示 */
public boolean isBorderVisible(String name) {
return borderShowing
&& name.equals(borderRegionName);
}
private Location getP(World w, int x, int z) {
int y = w.getHighestBlockYAt(x, z) + 2;
return new Location(w, x + 0.5, y, z + 0.5);
}
/**
* 格式:
* DIAMOND,1,10
* 债券,50,10
* 命令,give {player} diamond 1,5
* 贴标:STICK,1,60,KNOCKBACK,2,鸡腿棒,10
* 物品,数量,权重,附魔,等级,显示名,时长秒
*/
private TreasureReward parseReward(String line) {
plugin.getLogger().info(
"[寻宝] 解析奖励行: " + line);
if (line.startsWith("贴标:")) {
TreasureReward r = parseCustomItem(
line.substring("贴标:".length()));
plugin.getLogger().info(
"[寻宝] → 贴标结果: "
+ (r != null ? "OK" : "NULL"));
return r;
}
// ★ 统一分隔符:中文逗号→英文逗号
String normalized = line
.replace(",", ",")
.replace("。", ".")
.replace(":", ":")
.replace(";", ";");
String[] p = normalized.split(",");
if (p.length < 2) {
plugin.getLogger().warning(
"[寻宝] ★参数不足: "
+ p.length + "个");
return null;
}
// ★ 去首尾空格 + 转大写
String type = p[0].trim().toUpperCase();
// 债券
if (type.equals("债券")) {
int amt = Integer.parseInt(p[1].trim());
int w = p.length > 2
? Integer.parseInt(p[2].trim())
: 10;
TreasureReward r = new TreasureReward(
TreasureReward.Type.BOND,
null, 1, amt, null, w,
"§6" + amt + "债券");
plugin.getLogger().info(
"[寻宝] → 债券: " + amt
+ " 权重=" + w);
return r;
}
// 命令
if (type.equals("命令")) {
String cmd = p[1].trim();
int w = p.length > 2
? Integer.parseInt(p[2].trim())
: 10;
TreasureReward r = new TreasureReward(
TreasureReward.Type.COMMAND,
null, 1, 0, cmd, w,
"§a命令奖励");
plugin.getLogger().info(
"[寻宝] → 命令: " + cmd);
return r;
}
// ★ 物品名转大写
String matName = type;
int amt = Integer.parseInt(p[1].trim());
int w = p.length > 2
? Integer.parseInt(p[2].trim())
: 10;
Material m = Material.matchMaterial(matName);
if (m == null) {
plugin.getLogger().warning(
"[寻宝] ★物品识别失败: "
+ matName);
} else {
plugin.getLogger().info(
"[寻宝] → 物品: " + m.name()
+ " x" + amt
+ " 权重=" + w);
}
String dn = m != null
? "§f" + m.name() : "§f" + matName;
return new TreasureReward(
TreasureReward.Type.ITEM,
matName, amt, 0, null, w, dn);
}
/**
* 贴标物品解析
* 格式: 物品ID,数量,权重,附魔ID,附魔等级,显示名,时长秒
* 例: STICK,1,60,KNOCKBACK,2,鸡腿棒,3600
*/
private TreasureReward parseCustomItem(String line) {
String[] p = line.split(",");
if (p.length < 6) return null;
String matName = p[0].trim();
int amt = Integer.parseInt(p[1].trim());
int weight = Integer.parseInt(p[2].trim());
String enchId = p[3].trim();
int enchLvl = Integer.parseInt(p[4].trim());
String displayName = p[5].trim();
int duration = p.length >= 7
? parseTime(p[6].trim()) : 3600;
Material mat =
Material.matchMaterial(matName);
if (mat == null) mat = Material.STICK;
String enchant = enchId + "," + enchLvl;
String lore = "§7" + displayName;
return new TreasureReward(
TreasureReward.Type.ITEM,
matName, amt, 0, null, weight,
"§b" + displayName,
duration, enchant, lore);
}
// ========== 保存区域 ==========
public boolean saveRegion(String name,
String world,
int x1, int z1,
int x2, int z2,
int minY, int maxY) {
int a = Math.min(x1, x2);
int b = Math.max(x1, x2);
x1 = a; x2 = b;
a = Math.min(z1, z2);
b = Math.max(z1, z2);
z1 = a; z2 = b;
File dir = new File(
plugin.getDataFolder(), "寻宝");
dir.mkdirs();
File f = new File(dir, name + ".txt");
List<String> lines = new ArrayList<>();
lines.add("区域名: " + name);
lines.add("世界: " + world);
lines.add("坐标: " + x1 + "," + z1
+ "," + x2 + "," + z2);
lines.add("高度: " + minY + "," + maxY);
lines.add("最多宝箱: 3");
lines.add("检查间隔: 5分钟");
lines.add("生成概率: 100");
lines.add("奖励: DIAMOND,1,10");
lines.add("奖励: GOLD_INGOT,3,20");
lines.add("奖励: 债券,50,25");
try {
OutputStreamWriter w =
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8);
for (String l : lines)
w.write(l + "\n");
w.close();
} catch (IOException e) {
return false;
}
loadAll();
return true;
}
/** 标准化坐标:确保 minX <= maxX, minZ <= maxZ */
private void normalize(TreasureConfig tc) {
int a = Math.min(tc.minX, tc.maxX);
int b = Math.max(tc.minX, tc.maxX);
tc.minX = a;
tc.maxX = b;
a = Math.min(tc.minZ, tc.maxZ);
b = Math.max(tc.minZ, tc.maxZ);
tc.minZ = a;
tc.maxZ = b;
}
public boolean removeRegion(String name) {
File f = new File(plugin.getDataFolder(),
"寻宝/" + name + ".txt");
boolean ok = f.exists() && f.delete();
loadAll();
return ok;
}
// ★ expand: 四周各扩N格
public boolean expandRegion(String name, int n) {
TreasureConfig tc = configs.get(name);
if (tc == null) return false;
tc.minX -= n;
tc.minZ -= n;
tc.maxX += n;
tc.maxZ += n;
rewriteFile(tc);
return true;
}
// ★ return: 四周各缩N格
public boolean returnRegion(String name, int n) {
TreasureConfig tc = configs.get(name);
if (tc == null) return false;
if (tc.maxX - tc.minX <= n * 2
|| tc.maxZ - tc.minZ <= n * 2) {
return false;
}
tc.minX += n;
tc.minZ += n;
tc.maxX -= n;
tc.maxZ -= n;
rewriteFile(tc);
return true;
}
private void rewriteFile(TreasureConfig tc) {
File f = new File(plugin.getDataFolder(),
"寻宝/" + tc.name + ".txt");
List<String> lines = new ArrayList<>();
lines.add("区域名: " + tc.name);
lines.add("世界: " + tc.world);
lines.add("坐标: " + tc.minX + ","
+ tc.minZ + "," + tc.maxX
+ "," + tc.maxZ);
lines.add("高度: " + tc.minY + ","
+ tc.maxY);
lines.add("最多宝箱: " + tc.maxChests);
lines.add("检查间隔: " + tc.spawnInterval);
lines.add("生成概率: " + tc.spawnChance);
lines.add("随机高度: "
+ (tc.randomY ? "是" : "否"));
for (TreasureReward r : tc.rewards) {
switch (r.getType()) {
case BOND:
lines.add("奖励: 债券,"
+ r.getBondAmount()
+ "," + r.getWeight());
break;
case COMMAND:
lines.add("奖励: 命令,"
+ r.getCommand()
+ "," + r.getWeight());
break;
case ITEM:
lines.add("奖励: "
+ r.getMaterialName()
+ "," + r.getAmount()
+ "," + r.getWeight());
break;
}
}
try {
OutputStreamWriter w =
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8);
for (String l : lines)
w.write(l + "\n");
w.close();
} catch (IOException ignored) {
}
}
// ========== 宝箱操作 ==========
public int getChestCount(String name) {
int c = 0;
for (String v : activeChests.values()) {
if (v.equals(name)) c++;
}
return c;
}
public boolean spawnChest(TreasureConfig tc) {
if (getChestCount(tc.name) >= tc.maxChests)
return false;
World w = plugin.getServer()
.getWorld(tc.world);
if (w == null) return false;
normalize(tc);
int x = ThreadLocalRandom.current()
.nextInt(tc.minX, tc.maxX + 1);
int z = ThreadLocalRandom.current()
.nextInt(tc.minZ, tc.maxZ + 1);
// ★ 在Y范围内找可放置的位置
int y = findPlaceableY(w, x, z,
tc.minY, tc.maxY);
if (y < 0) {
plugin.getLogger().warning(
"[寻宝] 找不到位置: [" + x
+ "," + z + "] Y范围="
+ tc.minY + "~"
+ tc.maxY);
return false;
}
Location loc = new Location(w, x, y, z);
loc.getBlock().setType(Material.CHEST);
activeChests.put(loc, tc.name);
plugin.getLogger().info(
"[寻宝] 宝箱:"
+ " " + tc.name
+ " [" + x + "," + y
+ "," + z + "]"
+ " X=" + tc.minX
+ "~" + tc.maxX
+ " Z=" + tc.minZ
+ "~" + tc.maxZ
+ " Y=" + tc.minY
+ "~" + tc.maxY);
Bukkit.broadcastMessage(
"§e§l[寻宝]§r §7" + tc.name
+ " §e区域刷新了宝箱!");
return true;
}
/**
* 在Y范围内找可放箱子的位置
* 从Y范围底部向上找:
* 找到实心方块 → 箱子放它上面
* 上方有2格空气(箱子+开箱空间)→ 可用
*/
private int findPlaceableY(World w, int x, int z,
int minY, int maxY) {
List<Integer> validYs = new ArrayList<>();
// 第一轮:找实心方块上放箱子
for (int y = minY; y <= maxY; y++) {
Block ground = w.getBlockAt(x, y, z);
Block chest = w.getBlockAt(x, y + 1, z);
Block above = w.getBlockAt(x, y + 2, z);
if (ground.getType().isSolid()
&& !chest.getType().isSolid()
&& above.getType() == Material.AIR) {
validYs.add(y + 1);
}
}
// 第二轮兜底:找空气+上方空气
if (validYs.isEmpty()) {
for (int y = minY; y <= maxY; y++) {
Block here = w.getBlockAt(x, y, z);
Block above2 = w.getBlockAt(
x, y + 1, z);
if (here.getType() == Material.AIR
&& above2.getType()
== Material.AIR) {
validYs.add(y);
}
}
}
if (validYs.isEmpty()) return -1;
return validYs.get(
ThreadLocalRandom.current()
.nextInt(validYs.size()));
}
// ★ 物品中文名映射
private static final Map<String, String> ITEM_NAMES
= new HashMap<>();
static {
ITEM_NAMES.put("DIAMOND", "§b钻石");
ITEM_NAMES.put("DIAMOND_ORE", "§b钻石矿石");
ITEM_NAMES.put("GOLD_INGOT", "§6金锭");
ITEM_NAMES.put("GOLD_NUGGET", "§6金粒");
ITEM_NAMES.put("IRON_INGOT", "§f铁锭");
ITEM_NAMES.put("EMERALD", "§a绿宝石");
ITEM_NAMES.put("LAPIS_LAZULI", "§9青金石");
ITEM_NAMES.put("REDSTONE", "§4红石");
ITEM_NAMES.put("COAL", "§8煤炭");
ITEM_NAMES.put("NETHERITE_INGOT", "§8下界合金锭");
ITEM_NAMES.put("NETHERITE_SCRAP", "§8下界合金碎片");
ITEM_NAMES.put("DIAMOND_SWORD", "§b钻石剑");
ITEM_NAMES.put("DIAMOND_PICKAXE", "§b钻石镐");
ITEM_NAMES.put("DIAMOND_AXE", "§b钻石斧");
ITEM_NAMES.put("DIAMOND_SHOVEL", "§b钻石铲");
ITEM_NAMES.put("DIAMOND_HELMET", "§b钻石头盔");
ITEM_NAMES.put("DIAMOND_CHESTPLATE", "§b钻石胸甲");
ITEM_NAMES.put("DIAMOND_LEGGINGS", "§b钻石护腿");
ITEM_NAMES.put("DIAMOND_BOOTS", "§b钻石靴子");
ITEM_NAMES.put("GOLDEN_SWORD", "§6金剑");
ITEM_NAMES.put("GOLDEN_APPLE", "§6金苹果");
ITEM_NAMES.put("ENCHANTED_GOLDEN_APPLE",
"§6附魔金苹果");
ITEM_NAMES.put("STICK", "§a木棍");
ITEM_NAMES.put("BREAD", "§6面包");
ITEM_NAMES.put("COOKED_BEEF", "§6熟牛排");
ITEM_NAMES.put("COOKED_PORKCHOP", "§6熟猪排");
ITEM_NAMES.put("COOKED_CHICKEN", "§6熟鸡肉");
ITEM_NAMES.put("ARROW", "§f箭矢");
ITEM_NAMES.put("ENDER_PEARL", "§5末影珍珠");
ITEM_NAMES.put("BLAZE_ROD", "§6烈焰棒");
ITEM_NAMES.put("GHAST_TEAR", "§f恶魂之泪");
ITEM_NAMES.put("SHULKER_SHELL", "§5潜影壳");
ITEM_NAMES.put("EXPERIENCE_BOTTLE",
"§a经验瓶");
ITEM_NAMES.put("TOTEM_OF_UNDYING",
"§6不死图腾");
ITEM_NAMES.put("ELYTRA", "§8鞘翅");
ITEM_NAMES.put("TRIDENT", "§b三叉戟");
ITEM_NAMES.put("BEACON", "§f信标");
}
/** 获取物品中文显示名 */
public static String getItemDisplayName(
String materialName) {
String upper = materialName.toUpperCase();
if (ITEM_NAMES.containsKey(upper)) {
return ITEM_NAMES.get(upper);
}
// 转为可读名:DIAMOND_ORE → Diamond Ore
String readable = materialName
.replace("_", " ")
.toLowerCase();
StringBuilder sb = new StringBuilder();
for (String word : readable.split(" ")) {
if (!word.isEmpty()) {
sb.append(Character
.toUpperCase(word.charAt(0)));
sb.append(word.substring(1));
sb.append(" ");
}
}
return "§f" + sb.toString().trim();
}
// ★ 区域边框存储
private final Map<String,
Map<Location, Material>> regionBorders
= new HashMap<>();
private void setBorderBlock(World w, int x,
int z,
Map<Location,
Material> blocks) {
int y = w.getHighestBlockYAt(x, z) + 1;
Location loc = new Location(w, x, y, z);
Block block = w.getBlockAt(loc);
// 记录原方块
if (!blocks.containsKey(loc)) {
blocks.put(loc, block.getType());
}
// 放置荧石
block.setType(Material.GLOWSTONE);
}
public boolean isTreasure(Location loc) {
return activeChests.containsKey(loc);
}
public void removeChest(Location loc) {
activeChests.remove(loc);
}
public Map<String, TreasureConfig> getConfigs() {
return configs;
}
public Map<Location, String> getActiveChests() {
return activeChests;
}
public List<String> getRegionNames() {
return new ArrayList<>(configs.keySet());
}
}
+198
View File
@@ -0,0 +1,198 @@
package Sdf1_game;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import net.kyori.adventure.text.Component;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.util.*;
public class TreasureRegion {
public enum State {
NONE, POS1_SET
}
private static final Map<String, TreasureRegion>
players = new HashMap<>();
private State state = State.NONE;
private Location pos1;
private Location pos2;
private int expandAmount = 5;
private String regionName;
// ★ 粒子边框任务
private BukkitTask particleTask;
private boolean borderVisible = false;
private static final Material TOOL =
Material.WOODEN_AXE;
public static TreasureRegion get(String name) {
return players.computeIfAbsent(
name, k -> new TreasureRegion());
}
public State getState() { return state; }
public Location getPos1() { return pos1; }
public Location getPos2() { return pos2; }
public int getExpand() { return expandAmount; }
public boolean isBorderVisible() {
return borderVisible;
}
public void setPos1(Location loc) {
this.pos1 = loc.clone();
this.state = State.POS1_SET;
}
public void setPos2(Location loc) {
this.pos2 = loc.clone();
}
public void setExpand(int val) {
this.expandAmount = val;
}
public void reset() {
hideBorder();
this.state = State.NONE;
this.pos1 = null;
this.pos2 = null;
this.expandAmount = 5;
this.regionName = null;
}
/** 计算最终区域范围 */
public int[] getFinalBounds() {
if (pos1 == null || pos2 == null)
return null;
int minX = Math.min(pos1.getBlockX(),
pos2.getBlockX()) - expandAmount;
int maxX = Math.max(pos1.getBlockX(),
pos2.getBlockX()) + expandAmount;
int minZ = Math.min(pos1.getBlockZ(),
pos2.getBlockZ()) - expandAmount;
int maxZ = Math.max(pos1.getBlockZ(),
pos2.getBlockZ()) + expandAmount;
return new int[]{minX, minZ, maxX, maxZ};
}
// ★ 粒子边框:显示
public void showBorder() {
if (pos1 == null || pos2 == null) return;
hideBorder();
World w = pos1.getWorld();
if (w == null) return;
int[] b = getFinalBounds();
if (b == null) return;
final int minX = b[0], minZ = b[1];
final int maxX = b[2], maxZ = b[3];
borderVisible = true;
particleTask = new BukkitRunnable() {
int tick = 0;
@Override
public void run() {
tick++;
// 每4tick画一次(频率控制)
if (tick % 4 != 0) return;
// 收集所有边框点
List<Location> points =
new ArrayList<>();
// X轴两条边
for (int x = minX; x <= maxX; x++) {
points.add(getParticleLoc(
w, x, minZ));
points.add(getParticleLoc(
w, x, maxZ));
}
// Z轴两条边(不重复角)
for (int z = minZ + 1; z < maxZ; z++) {
points.add(getParticleLoc(
w, minX, z));
points.add(getParticleLoc(
w, maxX, z));
}
// 向范围内每个玩家发送粒子
for (Player p : w.getPlayers()) {
int px = p.getLocation()
.getBlockX();
int pz = p.getLocation()
.getBlockZ();
// 只给区域附近50格内的玩家
if (px >= minX - 50
&& px <= maxX + 50
&& pz >= minZ - 50
&& pz <= maxZ + 50) {
for (Location loc : points) {
p.spawnParticle(
Particle.FLAME,
loc, 1, 0, 0, 0, 0);
}
}
}
}
}.runTaskTimer(
org.bukkit.Bukkit.getPluginManager()
.getPlugin("Sdf1_game"),
0L, 20L);
}
private Location getParticleLoc(World w,
int x, int z) {
int y = w.getHighestBlockYAt(x, z) + 2;
return new Location(w, x + 0.5, y,
z + 0.5);
}
// ★ 粒子边框:隐藏
public void hideBorder() {
if (particleTask != null) {
particleTask.cancel();
particleTask = null;
}
borderVisible = false;
}
// ===== 工具 =====
public static void giveTool(Player p) {
ItemStack axe = new ItemStack(TOOL);
ItemMeta meta = axe.getItemMeta();
if (meta != null) {
meta.displayName(Component.text(
"§6§l圈地工具 §7- 左键A点右键B点"));
meta.lore(Arrays.asList(
Component.text("§7左键方块 = A点"),
Component.text("§7右键方块 = B点")));
axe.setItemMeta(meta);
}
p.getInventory().addItem(axe);
}
public static boolean isTool(ItemStack item) {
if (item == null) return false;
if (item.getType() != TOOL) return false;
ItemMeta meta = item.getItemMeta();
if (meta == null) return false;
return meta.hasDisplayName()
&& meta.getDisplayName().toString()
.contains("圈地工具");
}
}
+148
View File
@@ -0,0 +1,148 @@
package Sdf1_game;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.List;
public class TreasureReward {
public enum Type {
ITEM, BOND, COMMAND
}
private final Type type;
private final String materialName;
private final int amount;
private final int bondAmount;
private final String command;
private final int weight;
private final String displayName;
private final int durationSec;
private final String enchant;
private final String rewardLore;
public TreasureReward(Type type,
String materialName,
int amount,
int bondAmount,
String command,
int weight,
String displayName,
int durationSec,
String enchant,
String rewardLore) {
this.type = type;
this.materialName = materialName;
this.amount = Math.max(1, amount);
this.bondAmount = bondAmount;
this.command = command;
this.weight = weight;
this.displayName = displayName;
this.durationSec = durationSec;
this.enchant = enchant;
this.rewardLore = rewardLore;
}
public TreasureReward(Type type,
String materialName,
int amount,
int bondAmount,
String command,
int weight,
String displayName) {
this(type, materialName, amount, bondAmount,
command, weight, displayName,
0, null, null);
}
public Type getType() { return type; }
public String getMaterialName() { return materialName; }
public int getAmount() { return amount; }
public int getBondAmount() { return bondAmount; }
public String getCommand() { return command; }
public int getWeight() { return weight; }
public String getDisplayName() { return displayName; }
public int getDurationSec() { return durationSec; }
public String getEnchant() { return enchant; }
public ItemStack toItemStack() {
Material mat = Material.PAPER;
if (materialName != null
&& !materialName.isEmpty()) {
Material found =
Material.matchMaterial(materialName);
if (found != null) mat = found;
}
ItemStack item = new ItemStack(mat, amount);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.displayName(
Component.text(displayName));
List<Component> lore = new ArrayList<>();
if (durationSec > 0) {
lore.add(Component.text(
"§7时限: " + formatTime(durationSec)));
}
if (rewardLore != null
&& !rewardLore.isEmpty()) {
lore.add(Component.text(rewardLore));
}
lore.add(Component.text("§7点击领取"));
meta.lore(lore);
applyEnchant(meta, enchant);
item.setItemMeta(meta);
}
return item;
}
/** ★ 安全附魔:用 NamespacedKey */
static void applyEnchant(ItemMeta meta,
String enchant) {
if (enchant == null || enchant.isEmpty())
return;
String[] ep = enchant.split(",");
if (ep.length != 2) return;
try {
// 支持 "KNOCKBACK" 或 "minecraft:knockback"
String key = ep[0].trim();
if (!key.contains(":")) {
key = "minecraft:" + key.toLowerCase();
}
NamespacedKey nk =
NamespacedKey.fromString(key);
if (nk == null) return;
Enchantment ench =
Enchantment.getByKey(nk);
if (ench == null) return;
int lvl = Integer.parseInt(ep[1].trim());
meta.addEnchant(ench, lvl, true);
meta.addItemFlags(
ItemFlag.HIDE_ENCHANTS);
} catch (Exception ignored) {
}
}
static String formatTime(int sec) {
if (sec >= 3600) {
int h = sec / 3600;
int m = (sec % 3600) / 60;
return m > 0 ? h + "小时" + m + "分钟"
: h + "小时";
}
int m = sec / 60;
int s = sec % 60;
return s > 0 ? m + "分钟" + s + "秒"
: m + "分钟";
}
}
@@ -0,0 +1,28 @@
package Sdf1_game;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.concurrent.ThreadLocalRandom;
public class TreasureSpawnTask extends BukkitRunnable {
private final Main plugin;
private final TreasureManager manager;
public TreasureSpawnTask(Main plugin,
TreasureManager manager) {
this.plugin = plugin;
this.manager = manager;
}
@Override
public void run() {
for (TreasureConfig tc
: manager.getConfigs().values()) {
int roll = ThreadLocalRandom.current()
.nextInt(100);
if (roll >= tc.spawnChance) continue;
manager.spawnChest(tc);
}
}
}
+22 -4
View File
@@ -2,17 +2,35 @@ name: Sdf1_game
version: 1.0
main: Sdf1_game.Main
api-version: '1.21'
description: 快问快答 - 自动答题赢债券
description: 快问快答+寻宝系统
softdepend: [Sdf1_login]
commands:
quiz:
description: 快问快答管理
usage: /quiz <reload|start|stop|test>
description: 快问快答
usage: /quiz
permission: sdf1_quiz.admin
aliases: [出题]
出题:
description: 快问快答出题
usage: /出题 [选择题1]
usage: /出题
permission: sdf1_quiz.admin
treasure:
description: 寻宝管理
usage: /treasure
permission: sdf1_treasure.admin
寻宝:
description: 寻宝管理
usage: /寻宝
permission: sdf1_treasure.admin
跳过:
description: 跳过本轮答题
usage: /跳过
permissions:
sdf1_quiz.admin:
default: op
sdf1_treasure.admin:
default: op
sdf1_treasure.open:
default: true