feat: 新增寻宝功能,修复题目重载后不生效的问题
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+711
-229
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(
|
||||
"[快问快答] 加载失败: "
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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("圈地工具");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user