feat: 新增 Sdf1_game 插件初始版本(快问块答小游戏,对接 Sdf1_login 债券系统)

This commit is contained in:
ypsdf1
2026-06-01 20:35:02 +08:00
commit 55996f3b2b
14 changed files with 2250 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
+10
View File
@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+90
View File
@@ -0,0 +1,90 @@
# Sdf1_game插件说明文档
# 特别说明:本插件仅有简中语言版本,如需除简中以外的任何版本,请下载插件源码,自行修改!
## 玩法介绍:
| 名字 | 实现状态 |
| :---: | :---: |
| 快问块答 | 已实现 |
| 挖土掘金 | 待实现 |
| 寻宝 | 待实现 |
## 必装依赖
- [Sdf1_login](https://github.com/ypsdf1/Sdf1_login) 用途:经济提供者
## 指令介绍
| 指令 | 参数1 | 参数2 | 参数3 | 结果 |
| :---: | :---: | :---: | :---: | :---: |
| quiz | 无 | 无 | 无 | 帮助 |
| quiz | start | 无 | 无 | 出题 |
| quiz | info | 无 | 无 | 状态 |
| quiz | reload | 无 | 无 | 重载 |
| quiz | start | 题型 | 题目内容/编号(从上到下第N题) | 出指定的题目 |
## 配置文件:
```text
# ===== 快问快答 主配置 =====
# 支持 # // /* */ <!-- --> 注释
# 运行模式: 自动 / 手动
# 模式区别:手动模式:纯手动出题,自动模式是手自一体化
模式: 自动
# 最少在线人数
最少在线人数: 3
# 出题间隔
# 支持: 秒(s) 分钟(m) 小时(h)
最小间隔: 5分钟
最大间隔: 15分钟
# 答题时间
答题时间: 2分钟
# 蒙题防护
# 选择题中,2秒内错误答案超过此数则罚时
# 罚时期间答案无效
蒙题检测数: 2
# 罚时时间(秒)
# 支持: 秒(s) 分钟(m) 小时(h)
罚时时间: 10秒
# 消息前缀
消息前缀: §e§l【快问快答】§r
# 蒙题防护提示
罚时开始提示: §c你回答太快了,请冷静10秒!
罚时结束提示: §a罚时已结束,你可以继续答题了
```
## 题目支持:
- 选择题
- 填空题
- 问答题
## 题目配置格式:
```text
# 编号. 问题(赏金1)(赏金2)...
# 答案:答案1,答案2,...
1. 钻石矿在( )层以下生成。(10)
答案:16
2. 末影龙被击败后掉落( )经验。(15)
答案:12000
# ===== 问答题 =====
# | 问题 | 答案 | 赏金 |
| 问题 | 答案 | 赏金 |
|------|------|------|
| MC的全称是什么? | Minecraft | 20 |
| 下界合金锭的英文名? | Netherite Ingot | 15 |
| 末影螨的英文名? | Endermite | 12 |
| 信标的英文名? | Beacon | 10 |
```
+48
View File
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>sdf1</groupId>
<artifactId>Sdf1_game</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>Sdf1_game</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
+112
View File
@@ -0,0 +1,112 @@
package Sdf1_game;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class BondBridge {
private Object bondManager;
private Method addBondsMethod;
private boolean hooked = false;
public boolean hook(Main plugin) {
Plugin sdf1 = Bukkit.getPluginManager()
.getPlugin("Sdf1_login");
if (sdf1 == null || !sdf1.isEnabled()) {
plugin.getLogger().warning(
"[快问快答] 未找到Sdf1_login,"
+ "债券奖励不可用");
return false;
}
try {
bondManager = findManager(sdf1);
if (bondManager == null) {
plugin.getLogger().warning(
"[快问快答] 找不到BondManager");
return false;
}
addBondsMethod =
bondManager.getClass()
.getMethod("addBonds",
String.class,
int.class,
String.class,
String.class,
String.class,
String.class);
hooked = true;
plugin.getLogger().info(
"[快问快答] 债券桥接成功");
return true;
} catch (Exception e) {
plugin.getLogger().warning(
"[快问快答] 桥接失败: "
+ e.getMessage());
return false;
}
}
private Object findManager(Plugin sdf1) {
Class<?> c = sdf1.getClass();
// 策略1: getBondManager()
try {
Method m = c.getMethod("getBondManager");
Object r = m.invoke(sdf1);
if (r != null) return r;
} catch (Exception ignored) {
}
// 策略2: 字段 bondManager
try {
Field f = c.getDeclaredField(
"bondManager");
f.setAccessible(true);
Object r = f.get(sdf1);
if (r != null) return r;
} catch (Exception ignored) {
}
// 策略3: 遍历字段按类型名查找
try {
for (Field f : c.getDeclaredFields()) {
if (f.getType().getSimpleName()
.equals("BondManager")) {
f.setAccessible(true);
Object r = f.get(sdf1);
if (r != null) return r;
}
}
} catch (Exception ignored) {
}
return null;
}
public boolean addBonds(String player,
int amount,
String reason) {
if (!hooked) return false;
try {
int r = (int) addBondsMethod.invoke(
bondManager,
player, amount,
"quiz_reward", "",
"快问快答", reason);
return r >= 0;
} catch (Exception e) {
return false;
}
}
public boolean isHooked() {
return hooked;
}
}
+160
View File
@@ -0,0 +1,160 @@
package Sdf1_game;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class FileGenerator {
public static void ensureAll(File base, File qDir) {
writeIfAbsent(
new File(base, "配置.txt"),
defaultConfig());
writeIfAbsent(
new File(qDir, "选择题.md"),
defaultChoice());
writeIfAbsent(
new File(qDir, "填空题.md"),
defaultFill());
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);
for (String line : lines) {
w.write(line);
w.write("\n");
}
w.close();
} catch (IOException ignored) {
}
}
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("最小间隔: 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("# 消息前缀");
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 |");
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");
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 |");
return l;
}
}
+338
View File
@@ -0,0 +1,338 @@
package Sdf1_game;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main extends JavaPlugin
implements TabCompleter {
private BondBridge bondBridge;
private QuizManager quizManager;
private static final Pattern SPEC_NO_SPACE =
Pattern.compile("^(选择题|填空题|问答题)(\\d+)$");
private static final Pattern SPEC_WITH_SPACE =
Pattern.compile("^(选择题|填空题|问答题)\\s+(\\d+)$");
private static final List<String> TYPES =
Arrays.asList("选择题", "单选题",
"多选题", "填空题", "问答题");
@Override
public void onEnable() {
File base = new File(getDataFolder(), "快问快答");
File qDir = new File(base, "题目");
qDir.mkdirs();
FileGenerator.ensureAll(base, qDir);
bondBridge = new BondBridge();
bondBridge.hook(this);
quizManager = new QuizManager(this, bondBridge);
quizManager.loadQuestions();
getServer().getPluginManager()
.registerEvents(quizManager, this);
getCommand("quiz").setTabCompleter(this);
getCommand("出题").setTabCompleter(this);
quizManager.startAuto();
getLogger().info("=== 快问快答 v1.0.0 ===");
getLogger().info("模式: " + quizManager.getMode());
getLogger().info("债券桥接: "
+ (bondBridge.isHooked() ? "已连接" : "未连接"));
getLogger().info("\n" +
" ____ _ __ _ \n" +
" / ___| __| |/ _/ | __ _ __ _ _ __ ___ ___ \n" +
" \\___ \\ / _` | |_| | / _` |/ _` | '_ ` _ \\ / _ \\ \n" +
" ___) | (_| | _| | | (_| | (_| | | | | | | __/ \n" +
" |____/ \\__,_|_| |_|____\\__, |\\__,_|_| |_| |_|\\___| \n" +
" ____ _ __ |_____|___/ \n" +
" / ___| __| |/ _/ | \n" +
" \\___ \\ / _` | |_| | \n" +
" ___) | (_| | _| | \n" +
" |____/ \\__,_|_| |_| _ ____ _ __ _ \n" +
" _ __ _____ _____ _ __ | |__ _ _ / ___| __| |/ _/ |\n" +
" | '_ \\ / _ \\ \\ /\\ / / _ \\ '__| | '_ \\| | | | \\___ \\ / _` | |_| |\n" +
" | |_) | (_) \\ V V / __/ | | |_) | |_| | ___) | (_| | _| |\n" +
" | .__/ \\___/ \\_/\\_/ \\___|_| |_.__/ \\__, | |____/ \\__,_|_| |_|\n" +
" |_| |___/ ");
}
@Override
public void onDisable() {
if (quizManager != null) {
quizManager.stopAuto();
}
getLogger().info("[快问快答] 已卸载");
}
@Override
public boolean onCommand(CommandSender sender,
Command command,
String label,
String[] args) {
if (!sender.hasPermission("sdf1_quiz.admin")) {
sender.sendMessage("§c无权限");
return true;
}
String cmd = command.getName();
// ===== /出题 =====
if (cmd.equals("出题") || label.equals("出题")) {
handleStart(sender, args);
return true;
}
// ===== /quiz =====
if (!cmd.equals("quiz")) {
return false;
}
if (args.length == 0) {
sendHelp(sender);
return true;
}
switch (args[0].toLowerCase()) {
case "start":
handleStart(sender, args);
return true;
case "stop":
quizManager.stopAuto();
sender.sendMessage("§a已停止自动出题");
return true;
case "reload":
quizManager.reload();
sender.sendMessage("§a已重载");
return true;
case "info":
sendInfo(sender);
return true;
case "test":
sender.sendMessage(bondBridge.isHooked()
? "§a桥接: 已连接"
: "§c桥接: 未连接");
return true;
default:
sendHelp(sender);
return true;
}
}
private void sendHelp(CommandSender sender) {
sender.sendMessage("§e§l【快问快答】§r 命令:");
sender.sendMessage("§7/出题 - 随机出题");
sender.sendMessage("§7/出题 选择题1 - 指定题号");
sender.sendMessage("§7/出题 多选题2 - 多选题");
sender.sendMessage("§7/出题 填空题 钻石矿 - 按内容");
sender.sendMessage("§7/quiz stop - 停止自动");
sender.sendMessage("§7/quiz reload - 重载");
sender.sendMessage("§7/quiz info - 查看状态");
sender.sendMessage("§7/quiz test - 测试桥接");
}
private void sendInfo(CommandSender sender) {
sender.sendMessage("§e§l【快问快答】§r 状态:");
sender.sendMessage("§7模式: §e" + quizManager.getMode());
sender.sendMessage("§7答题中: §e"
+ (quizManager.isActive() ? "是" : "否"));
sender.sendMessage("§7选择题: §e" + quizManager.getChoiceCount()
+ " §7填空题: §e" + quizManager.getFillCount()
+ " §7问答题: §e" + quizManager.getOpenCount());
sender.sendMessage("§7债券桥接: "
+ (bondBridge.isHooked() ? "§a已连接" : "§c未连接"));
}
// ===== /出题 参数解析 =====
private void handleStart(CommandSender sender, String[] args) {
// 无参数 → 随机
if (args.length == 0) {
doStart(sender, null, 0, null);
return;
}
// /quiz start 无后续 → 随机
if (args.length == 1 && args[0].equalsIgnoreCase("start")) {
doStart(sender, null, 0, null);
return;
}
// 确定参数起始位置
int offset = 0;
if (args.length >= 2 && args[0].equalsIgnoreCase("start")) {
offset = 1;
}
// 拼接剩余参数
StringBuilder sb = new StringBuilder();
for (int i = offset; i < args.length; i++) {
if (sb.length() > 0) sb.append(" ");
sb.append(args[i]);
}
String spec = sb.toString().trim();
// 格式1: "选择题1" 无空格
Matcher m1 = SPEC_NO_SPACE.matcher(spec);
if (m1.matches()) {
doStart(sender, m1.group(1),
Integer.parseInt(m1.group(2)), null);
return;
}
// 格式2: "选择题 1" 有空格
Matcher m2 = SPEC_WITH_SPACE.matcher(spec);
if (m2.matches()) {
doStart(sender, m2.group(1),
Integer.parseInt(m2.group(2)), null);
return;
}
// 格式3: "选择题 钻石矿" 按内容搜索
for (String typeName : TYPES) {
if (spec.startsWith(typeName)) {
String keyword = spec.substring(typeName.length()).trim();
if (!keyword.isEmpty()) {
Question q = quizManager.findByKeyword(typeName, keyword);
if (q != null) {
doStart(sender, null, 0, q);
} else {
sender.sendMessage("§c未找到包含「" + keyword + "」的题目");
}
return;
}
}
}
sender.sendMessage("§c格式错误,示例:");
sender.sendMessage("§7/出题 - 随机出题");
sender.sendMessage("§7/出题 选择题1");
sender.sendMessage("§7/出题 选择题 1");
sender.sendMessage("§7/出题 选择题 钻石矿");
}
private void doStart(CommandSender sender,
String type, int index,
Question specific) {
// ★ 在线人数必须>=1
if (Bukkit.getOnlinePlayers().size() < 1) {
sender.sendMessage("§c服务器无人,无法出题");
return;
}
if (quizManager.isActive()) {
sender.sendMessage("§c当前有题目进行中");
return;
}
if (specific != null) {
quizManager.forceQuestion(specific);
sender.sendMessage("§a已出题: ["
+ specific.getTypeName() + "] "
+ specific.getQuestion());
} else if (type != null) {
quizManager.forceQuestion(type, index);
sender.sendMessage("§a已出题: "
+ type + "第" + index + "题");
} else {
quizManager.forceQuestion();
sender.sendMessage("§a已随机出题");
}
}
// ===== Tab 补全 =====
@Override
public List<String> onTabComplete(CommandSender sender,
Command command,
String label,
String[] args) {
if (!sender.hasPermission("sdf1_quiz.admin")) {
return new ArrayList<>();
}
List<String> result = new ArrayList<>();
String cmd = command.getName();
boolean isStart = cmd.equals("quiz")
&& args.length >= 1
&& args[0].equalsIgnoreCase("start");
// /出题 或 /quiz start 的参数
boolean isQuizSpec = cmd.equals("出题")
|| label.equals("出题")
|| isStart;
if (!isQuizSpec) {
// /quiz 的子命令补全
if (cmd.equals("quiz") && args.length == 1) {
String input = args[0].toLowerCase();
for (String s : Arrays.asList(
"start", "stop", "reload", "test", "info")) {
if (s.startsWith(input)) {
result.add(s);
}
}
}
return result;
}
// 确定参数位置
int argPos = 0;
if (cmd.equals("quiz") && isStart) {
argPos = args.length - 1;
} else {
argPos = args.length - 1;
}
if (argPos == 0) {
// 补全题型
String input = args[0].toLowerCase();
for (String t : TYPES) {
if (t.startsWith(input) || input.isEmpty()) {
result.add(t);
}
}
} else if (argPos == 1) {
// 补全题号或关键词
String typeName = args[0];
if (TYPES.contains(typeName)) {
int count = getCountForType(typeName);
for (int i = 1; i <= count; i++) {
result.add(String.valueOf(i));
}
}
}
return result;
}
private int getCountForType(String type) {
switch (type) {
case "选择题":
case "单选题":
return quizManager.getChoiceCount();
case "多选题":
return quizManager.getMultiCount();
case "填空题":
return quizManager.getFillCount();
case "问答题":
return quizManager.getOpenCount();
default: return 0;
}
}}
+111
View File
@@ -0,0 +1,111 @@
package Sdf1_game;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Question {
public enum Type {
CHOICE, MULTI, FILL, OPEN
}
private static final Pattern BLANK =
Pattern.compile("\\(\\s*\\)");
private final Type type;
private final String question;
private final String answer;
private final int[] rewards;
private final List<String> options;
public Question(Type type, String question,
String answer, int[] rewards,
List<String> options) {
this.type = type;
this.question = question;
this.answer = answer;
this.rewards = rewards;
this.options = options != null
? new ArrayList<>(options)
: new ArrayList<>();
}
public Question(Type type, String question,
String answer, int reward,
List<String> options) {
this(type, question, answer,
new int[]{reward}, options);
}
public Question(Type type, String question,
String answer, int reward) {
this(type, question, answer,
new int[]{reward}, null);
}
public Question(Type type, String question,
String answer, int[] rewards) {
this(type, question, answer,
rewards, new ArrayList<>());
}
public Type getType() {
return type;
}
public String getQuestion() {
return question;
}
public String getAnswer() {
return answer;
}
public int[] getRewards() {
return rewards;
}
public List<String> getOptions() {
return options;
}
public int getDisplayReward() {
int max = 0;
for (int r : rewards) {
max = Math.max(max, r);
}
return max;
}
public int getRewardForCount(int count) {
if (count <= 0) return 0;
return rewards[Math.min(count,
rewards.length) - 1];
}
public int getBlankCount() {
Matcher m = BLANK.matcher(question);
int c = 0;
while (m.find()) c++;
return c;
}
public int getCorrectCount() {
return answer.split(",").length;
}
public String getTypeName() {
switch (type) {
case CHOICE:
return "选择题";
case MULTI:
return "多选题";
case FILL:
return "填空题";
default:
return "问答题";
}
}
}
+357
View File
@@ -0,0 +1,357 @@
package Sdf1_game;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class QuestionLoader {
private static final Pattern REWARD_TRAIL =
Pattern.compile("\\((\\d+)\\)$");
public static List<Question> load(Main plugin,
String resourcePath) {
List<Question> questions = new ArrayList<>();
File file = new File(
plugin.getDataFolder(), resourcePath);
if (!file.exists()) {
plugin.getLogger().warning(
"[快问快答] 文件不存在: "
+ resourcePath);
return questions;
}
Question.Type type;
if (resourcePath.contains("选择题")) {
type = Question.Type.CHOICE;
} else if (resourcePath.contains("填空题")) {
type = Question.Type.FILL;
} else {
type = Question.Type.OPEN;
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(file),
StandardCharsets.UTF_8))) {
List<String> cleaned = cleanComments(br);
switch (type) {
case CHOICE:
parseChoice(cleaned, questions);
break;
case FILL:
parseFill(cleaned, questions);
break;
default:
parseOpen(cleaned, questions);
break;
}
} catch (IOException e) {
plugin.getLogger().warning(
"[快问快答] 加载失败: "
+ resourcePath + " - "
+ e.getMessage());
}
return questions;
}
private static List<String> cleanComments(
BufferedReader br) {
List<String> result = new ArrayList<>();
try {
String line;
boolean inBlock = false;
boolean inHtml = false;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
if (inBlock) {
int end = line.indexOf("*/");
if (end >= 0) {
inBlock = false;
line = line
.substring(end + 2)
.trim();
if (line.isEmpty()) continue;
} else {
continue;
}
}
if (line.startsWith("/*")) {
int end = line.indexOf("*/", 2);
if (end >= 0) {
line = line
.substring(end + 2)
.trim();
if (line.isEmpty()) continue;
} else {
inBlock = true;
continue;
}
}
if (inHtml) {
int end = line.indexOf("-->");
if (end >= 0) {
inHtml = false;
line = line
.substring(end + 3)
.trim();
if (line.isEmpty()) continue;
} else {
continue;
}
}
if (line.startsWith("<!--")) {
int end = line.indexOf("-->", 4);
if (end >= 0) {
line = line
.substring(end + 3)
.trim();
if (line.isEmpty()) continue;
} else {
inHtml = true;
continue;
}
}
if (line.startsWith("#")) continue;
int h = line.indexOf('#');
if (h > 0) {
line = line.substring(0, h).trim();
}
int s = line.indexOf("//");
if (s > 0) {
line = line.substring(0, s).trim();
}
if (!line.isEmpty()) {
result.add(line);
}
}
} catch (IOException ignored) {
}
return result;
}
/**
* 选择题表格
* 答案含逗号 → 多选题
* 答案为"全选" → 所有选项
*/
private static void parseChoice(
List<String> lines,
List<Question> result) {
for (String line : lines) {
if (!line.startsWith("|")) continue;
if (line.contains("---")) continue;
String[] parts = line.split("\\|");
if (parts.length < 5) continue;
String question = parts[1].trim();
String answer =
parts[parts.length - 2].trim();
String rewardStr =
parts[parts.length - 1].trim();
if (question.equals("问题")) continue;
if (question.isEmpty()
|| answer.isEmpty()) continue;
// 收集非空选项
List<String> options = new ArrayList<>();
for (int i = 2;
i <= parts.length - 3; i++) {
String opt = parts[i].trim();
if (!opt.isEmpty()) {
options.add(opt);
}
}
if (options.isEmpty()) continue;
// 判断是否多选
boolean isMulti =
answer.contains(",")
|| answer.contains(",")
|| answer.equals("全选");
if (isMulti) {
String realAnswer;
if (answer.equals("全选")) {
// 生成全选答案
StringBuilder sb =
new StringBuilder();
char ch = 'A';
for (int i = 0;
i < options.size(); i++) {
if (i > 0) sb.append(",");
sb.append(ch);
ch++;
}
realAnswer = sb.toString();
} else {
realAnswer = answer;
}
// 赏金解析
String[] rp = rewardStr.split(",");
int correctCount =
realAnswer.split(",").length;
int[] rewards;
if (rp.length > 1) {
rewards = new int[Math.min(
rp.length, correctCount)];
for (int i = 0;
i < rewards.length; i++) {
rewards[i] = Integer.parseInt(
rp[i].trim());
}
} else {
int base = Integer.parseInt(
rewardStr.trim());
rewards = new int[correctCount];
for (int i = 0;
i < correctCount; i++) {
rewards[i] = base * (i + 1);
}
}
result.add(new Question(
Question.Type.MULTI,
question, realAnswer,
rewards, options));
} else {
// 单选题
int reward;
try {
reward = Integer.parseInt(
rewardStr);
} catch (NumberFormatException e) {
continue;
}
result.add(new Question(
Question.Type.CHOICE,
question, answer,
reward, options));
}
}
}
private static void parseOpen(
List<String> lines,
List<Question> result) {
for (String line : lines) {
if (!line.startsWith("|")) continue;
if (line.contains("---")) continue;
String[] parts = line.split("\\|");
if (parts.length < 4) continue;
String question = parts[1].trim();
String answer = parts[2].trim();
String rewardStr = parts[3].trim();
if (question.equals("问题")) continue;
if (question.isEmpty()
|| answer.isEmpty()) continue;
int reward;
try {
reward = Integer.parseInt(rewardStr);
} catch (NumberFormatException e) {
continue;
}
result.add(new Question(
Question.Type.OPEN,
question, answer, reward));
}
}
private static void parseFill(
List<String> lines,
List<Question> result) {
int i = 0;
while (i < lines.size()) {
String line = lines.get(i);
if (!line.matches("\\d+\\.\\s*.*")) {
i++;
continue;
}
int dot = line.indexOf('.');
String content =
line.substring(dot + 1).trim();
List<Integer> rewardList =
new ArrayList<>();
while (true) {
Matcher m = REWARD_TRAIL
.matcher(content);
if (m.find()) {
rewardList.add(0,
Integer.parseInt(
m.group(1)));
content = content.substring(0,
m.start()).trim();
} else {
break;
}
}
if (content.isEmpty()) {
i++;
continue;
}
String answer = "";
if (i + 1 < lines.size()) {
String aLine =
lines.get(i + 1).trim();
int ci = Math.max(
aLine.indexOf(':'),
aLine.indexOf(':'));
if (ci >= 0) {
answer = aLine
.substring(ci + 1).trim();
}
i += 2;
} else {
i++;
continue;
}
if (answer.isEmpty()) continue;
int[] rewards = rewardList.stream()
.mapToInt(Integer::intValue)
.toArray();
result.add(new Question(
Question.Type.FILL,
content, answer, rewards));
}
}
}
+940
View File
@@ -0,0 +1,940 @@
package Sdf1_game;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class QuizManager implements Listener {
private final Main plugin;
private final BondBridge bondBridge;
private List<Question> choiceQ = new ArrayList<>();
private List<Question> fillQ = new ArrayList<>();
private List<Question> openQ = new ArrayList<>();
private BukkitTask autoTask;
private BukkitTask countTask;
private Question cur;
private boolean active = false;
private int timeLeft;
private boolean firstRun = true;
private long lastQuizTime = 0;
private boolean wasEmpty = false;
private String mode = "自动";
private final Set<UUID> answered =
Collections.synchronizedSet(new HashSet<>());
private final Object lock = new Object();
// 蒙题防护:记录每个玩家最近的错误答案时间
private final Map<UUID, List<Long>> wrongTimes =
new ConcurrentHashMap<>();
// 蒙题防护:罚时结束时间
private final Map<UUID, Long> punishUntil =
new ConcurrentHashMap<>();
private String prefix = "§e§l【快问快答】§r ";
private int minPlayers = 3;
private int minSec = 300;
private int maxSec = 900;
private int ansTime = 120;
private List<Question> multiQ = new ArrayList<>();
private int spamThreshold = 2;
private int punishSeconds = 10;
private String punishStartMsg =
"§c你回答太快了,请冷静10秒!";
private String punishEndMsg =
"§a罚时已结束,你可以继续答题了";
public QuizManager(Main plugin, BondBridge bb) {
this.plugin = plugin;
this.bondBridge = bb;
}
/**
* 时间单位自动换算
* 支持: 纯数字(秒) / 30秒 / 5分钟 / 2小时
* 单位: 秒(s) / 分钟(m) / 小时(h)
*/
private int parseTime(String input) {
String s = input.trim().toLowerCase();
// 纯数字 → 当作秒
if (s.matches("\\d+")) {
return Integer.parseInt(s);
}
// 提取数字部分和单位部分
// "5分钟" "30s" "2小时"
java.util.regex.Matcher m =
java.util.regex.Pattern
.compile("(\\d+)\\s*(秒|s|分|分钟|m|时|小时|h)")
.matcher(s);
if (m.find()) {
int val = Integer.parseInt(m.group(1));
String unit = m.group(2);
switch (unit) {
case "秒":
case "s":
return val;
case "分":
case "分钟":
case "m":
return val * 60;
case "时":
case "小时":
case "h":
return val * 3600;
}
}
// 解析失败,返回原值尝试 parseInt
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return 0;
}
}
// ========== 读配置 ==========
public void loadConfig() {
File f = new File(plugin.getDataFolder(),
"快问快答/配置.txt");
if (!f.exists()) return;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
if (line.startsWith("#")
|| line.startsWith("//")
|| line.startsWith("<!--"))
continue;
if (!line.contains(":")) continue;
int idx = line.indexOf(':');
String k = line.substring(0, idx).trim();
String v = line.substring(idx + 1).trim();
switch (k) {
case "模式":
mode = v;
break;
case "消息前缀":
prefix = v;
break;
case "最少在线人数":
minPlayers =
Integer.parseInt(v);
break;
case "最小间隔":
minSec = parseTime(v);
break;
case "最大间隔":
maxSec = parseTime(v);
break;
case "答题时间":
ansTime = parseTime(v);
break;
case "蒙题检测数":
spamThreshold =
Integer.parseInt(v);
break;
case "罚时时间":
punishSeconds = parseTime(v);
break;
case "罚时开始提示":
punishStartMsg = v;
break;
case "罚时结束提示":
punishEndMsg = v;
break;
}
}
} catch (IOException e) {
plugin.getLogger().warning(
"[快问快答] 读配置失败: "
+ e.getMessage());
}
plugin.getLogger().info(
"[快问快答] 模式=" + mode
+ " 最少" + minPlayers + "人"
+ " 间隔" + minSec + "-"
+ maxSec + "秒"
+ " 答题" + ansTime + "秒");
}
// ========== 加载题目 ==========
public void loadQuestions() {
loadConfig();
choiceQ = QuestionLoader.load(plugin,
"快问快答/题目/选择题.md");
multiQ = QuestionLoader.load(plugin,
"快问快答/题目/多选题.md");
fillQ = QuestionLoader.load(plugin,
"快问快答/题目/填空题.md");
openQ = QuestionLoader.load(plugin,
"快问快答/题目/问答题.md");
plugin.getLogger().info(
"[快问快答] 单选" + choiceQ.size()
+ " 多选" + multiQ.size()
+ " 填空" + fillQ.size()
+ " 问答" + openQ.size());
}
public int getMultiCount() {
return multiQ.size();
}
// ========== 自动出题 ==========
public void startAuto() {
stopAuto();
lastQuizTime = System.currentTimeMillis();
plugin.getLogger().info(
"[快问快答] 模式=" + mode
+ " 自动出题"
+ ("手动".equals(mode)
? "已跳过" : "已启动"));
if (!"手动".equals(mode)) {
startMonitor();
}
}
public void stopAuto() {
if (autoTask != null) {
autoTask.cancel();
autoTask = null;
}
}
/**
* 核心监控:每30秒检查一次
* - 有答题中 → 不管人数,继续
* - 无人数不足 → 静默跳过
* - 人数达标 + 距上次≥5分钟 → 出题
* - 之前没人现在有人了 → 立即出题
*/
private void startMonitor() {
if (autoTask != null) autoTask.cancel();
autoTask = new BukkitRunnable() {
@Override
public void run() {
int online =
Bukkit.getOnlinePlayers().size();
long now =
System.currentTimeMillis();
long elapsed = now - lastQuizTime;
// 答题中 → 不打断
if (active) return;
// 人数不足
if (online < minPlayers) {
wasEmpty = true;
return;
}
// 人数达标
// 情况A:之前没人,现在有人了 → 立即出题
if (wasEmpty) {
wasEmpty = false;
lastQuizTime = now;
broadcastQuestion();
return;
}
// 情况B:首次(还没出过题)
if (firstRun) {
// 启动后5分钟内不出题
if (elapsed < 300_000) return;
firstRun = false;
lastQuizTime = now;
broadcastQuestion();
return;
}
// 情况C:距上次出题≥设定间隔
long minInterval = minSec * 1000L;
if (elapsed >= minInterval) {
lastQuizTime = now;
broadcastQuestion();
}
}
}.runTaskTimer(plugin, 0L, 30 * 20L);
// 每30秒检查一次
}
private void scheduleNext() {
int delay;
if (firstRun) {
delay = 300;
firstRun = false;
plugin.getLogger().info(
"[快问快答] 首次定时: "
+ delay + "秒后检查");
} else {
delay = ThreadLocalRandom.current()
.nextInt(minSec, maxSec + 1);
plugin.getLogger().info(
"[快问快答] 下次检查: "
+ delay + "秒后");
}
autoTask = new BukkitRunnable() {
@Override
public void run() {
int online =
Bukkit.getOnlinePlayers().size();
plugin.getLogger().info(
"[快问快答] 定时检查: 在线="
+ online + " 需要>="
+ minPlayers
+ " 答题中=" + active);
if (online >= minPlayers && !active) {
broadcastQuestion();
}
scheduleNext();
}
}.runTaskLater(plugin, delay * 20L);
}
// ========== 出题 ==========
public void broadcastQuestion() {
broadcastQuestion(null, 0);
}
public void broadcastQuestion(String type,
int index) {
// ★ 拦截:在线人数<1时禁止出题
if (Bukkit.getOnlinePlayers().size() < 1) {
plugin.getLogger().warning(
"[快问快答] 在线人数不足,跳过出题");
return;
}
Question q = pickQuestion(type, index);
if (q == null) {
plugin.getLogger().warning(
"[快问快答] 抽题失败");
return;
}
startQuiz(q);
}
/**
* 外部强制出指定题目
*/
public void forceQuestion(Question q) {
if (!active) startQuiz(q);
}
/**
* 按关键词搜索题目
*/
public Question findByKeyword(String type,
String keyword) {
List<Question> pool;
switch (type) {
case "选择题":
pool = choiceQ;
break;
case "填空题":
pool = fillQ;
break;
case "问答题":
pool = openQ;
break;
default:
return null;
}
for (Question q : pool) {
if (q.getQuestion().contains(keyword)
|| q.getAnswer()
.contains(keyword)) {
return q;
}
}
return null;
}
private Question pickQuestion(String type,
int index) {
if (type != null) {
List<Question> pool;
switch (type) {
case "选择题":
case "单选题":
pool = choiceQ;
break;
case "多选题":
pool = multiQ;
break;
case "填空题":
pool = fillQ;
break;
case "问答题":
pool = openQ;
break;
default:
return null;
}
if (pool.isEmpty()) return null;
int idx = Math.max(0,
Math.min(index - 1,
pool.size() - 1));
return pool.get(idx);
}
List<Question> pool = new ArrayList<>();
pool.addAll(choiceQ);
pool.addAll(multiQ);
pool.addAll(fillQ);
pool.addAll(openQ);
if (pool.isEmpty()) return null;
return pool.get(
ThreadLocalRandom.current()
.nextInt(pool.size()));
}
// ========== 开始答题 ==========
private void startQuiz(Question q) {
synchronized (lock) {
cur = q;
active = true;
answered.clear();
}
timeLeft = ansTime;
StringBuilder msg = new StringBuilder();
msg.append(prefix);
switch (q.getType()) {
case CHOICE:
msg.append("§e§l[选择题]§r ")
.append(q.getQuestion());
char letter = 'A';
for (String opt : q.getOptions()) {
msg.append("\n§7")
.append(letter)
.append(". ")
.append(opt);
letter++;
}
msg.append("\n§e赏金: §6")
.append(q.getDisplayReward())
.append(" §e债券");
break;
case MULTI:
msg.append("§e§l[多选题]§r ")
.append(q.getQuestion());
char ml = 'A';
for (String opt : q.getOptions()) {
msg.append("\n§7")
.append(ml)
.append(". ")
.append(opt);
ml++;
}
msg.append("\n§7§o多个答案用逗号分隔"
+ ",如: A,B");
// 显示赏金明细
int[] rw = q.getRewards();
if (rw.length > 1) {
StringBuilder rb =
new StringBuilder();
for (int i = 0;
i < rw.length; i++) {
if (i > 0) rb.append("§7, ");
rb.append("§6答对")
.append(i + 1)
.append("个§e")
.append(rw[i])
.append("§6债券");
}
msg.append("\n§e赏金: ")
.append(rb);
} else {
msg.append("\n§e每个正确选项: §6")
.append(rw[0])
.append(" §e债券");
}
break;
case FILL:
msg.append("§e§l[填空题]§r ")
.append(q.getQuestion());
msg.append(
"\n§7多个答案用逗号分隔");
msg.append("\n§e赏金: §6")
.append(q.getDisplayReward())
.append(" §e债券(全对)");
break;
case OPEN:
msg.append("§e§l[问答题]§r ")
.append(q.getQuestion());
msg.append("\n§7直接输入答案");
msg.append("\n§e赏金: §6")
.append(q.getDisplayReward())
.append(" §e债券");
break;
}
msg.append("\n§e倒计时: §c")
.append(ansTime).append("秒");
plugin.getLogger().info(
"[快问快答] 出题: ["
+ q.getTypeName() + "] "
+ q.getQuestion());
Bukkit.broadcastMessage(msg.toString());
countTask = new BukkitRunnable() {
@Override
public void run() {
timeLeft--;
if (timeLeft <= 0) {
endQuiz(false);
return;
}
switch (timeLeft) {
case 90:
Bukkit.broadcastMessage(prefix
+ "§7还有1分30秒~");
break;
case 60:
Bukkit.broadcastMessage(prefix
+ "§7还剩1分钟!");
break;
case 30:
Bukkit.broadcastMessage(prefix
+ "§7还有30秒!");
break;
case 10:
Bukkit.broadcastMessage(prefix
+ "§c最后10秒!");
break;
}
if (timeLeft <= 5 && timeLeft >= 1) {
Bukkit.broadcastMessage(prefix
+ "§c" + timeLeft + "...");
}
}
}.runTaskTimer(plugin, 20L, 20L);
}
// ========== 结束 ==========
private void endQuiz(boolean hadWinner) {
Question snapshot;
synchronized (lock) {
snapshot = cur;
active = false;
cur = null;
}
if (countTask != null) {
countTask.cancel();
countTask = null;
}
if (!hadWinner) {
Bukkit.broadcastMessage(prefix
+ "§72分钟内无人答对,本题跳过~");
if (snapshot != null) {
Bukkit.broadcastMessage(prefix
+ "§e正确答案: §a"
+ snapshot.getAnswer());
String nextTime = getNextQuizTime();
Bukkit.broadcastMessage(prefix
+ "§7下次出题: §e"
+ nextTime);
}
}
}
// ========== 聊天监听 ==========
@EventHandler(priority = EventPriority.HIGHEST,
ignoreCancelled = true)
public void onChat(AsyncPlayerChatEvent event) {
Question q;
synchronized (lock) {
if (!active || cur == null) return;
q = cur;
}
Player player = event.getPlayer();
UUID uid = player.getUniqueId();
if (answered.contains(uid)) return;
String msg = event.getMessage().trim();
if (msg.isEmpty()) return;
// ★ 蒙题防护:罚时期间直接跳过
if (isPunished(uid)) {
player.sendMessage(punishStartMsg);
return;
}
// 只在选择题/多选题时检测蒙题
if (q.getType() == Question.Type.CHOICE
|| q.getType() == Question.Type.MULTI) {
boolean correct = isAnswerCorrect(msg, q);
if (!correct) {
// 记录错误时间
recordWrong(uid);
// 检查是否触发罚时
if (shouldPunish(uid)) {
applyPunish(uid, player);
}
}
}
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())) {
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;
event.setCancelled(true);
answered.add(uid);
boolean bondOk = false;
if (bondBridge.isHooked()) {
String reason = q.getTypeName() + "答对"
+ (q.getType()
== Question.Type.FILL
? " " + detail : "");
bondOk = bondBridge.addBonds(
player.getName(), reward, reason);
}
String info = bondOk
? "§6" + reward + " §e债券"
: "§7(债券未连接)";
Bukkit.broadcastMessage(prefix
+ "§a" + player.getName()
+ " §7答对了§e["
+ q.getTypeName() + "]§7!");
Bukkit.broadcastMessage(prefix
+ "§e正确答案: §a" + q.getAnswer());
Bukkit.broadcastMessage(prefix
+ "§e赏金: " + info);
Bukkit.broadcastMessage(prefix
+ "§7下次出题: §e"
+ getNextQuizTime());
endQuiz(true);
}
// ========== 蒙题防护 ==========
private boolean isAnswerCorrect(String input,
Question q) {
switch (q.getType()) {
case CHOICE:
return checkChoice(input, q);
case MULTI:
return countMultiCorrect(input, q) > 0;
default:
return true;
}
}
private void recordWrong(UUID uid) {
long now = System.currentTimeMillis();
List<Long> times = wrongTimes
.computeIfAbsent(uid,
k -> new ArrayList<>());
times.add(now);
// 只保留最近3秒内的记录
times.removeIf(t -> now - t > 3000);
}
private boolean shouldPunish(UUID uid) {
List<Long> times = wrongTimes.get(uid);
if (times == null) return false;
long now = System.currentTimeMillis();
// 2秒内错误次数 > 阈值
long cutoff = now - 2000;
int recentWrong = 0;
for (long t : times) {
if (t > cutoff) recentWrong++;
}
return recentWrong > spamThreshold;
}
private void applyPunish(UUID uid,
Player player) {
long end = System.currentTimeMillis()
+ (long) punishSeconds * 1000;
punishUntil.put(uid, end);
wrongTimes.remove(uid);
player.sendMessage(punishStartMsg);
// 罚时结束后自动解除
final UUID punishUid = uid;
final Player punishPlayer = player;
new BukkitRunnable() {
@Override
public void run() {
punishUntil.remove(punishUid);
if (punishPlayer.isOnline()) {
punishPlayer.sendMessage(
punishEndMsg);
}
}
}.runTaskLater(plugin, punishSeconds * 20L);
plugin.getLogger().info(
"[快问快答] 蒙题罚时: "
+ player.getName()
+ " " + punishSeconds + "秒");
}
private boolean isPunished(UUID uid) {
Long until = punishUntil.get(uid);
if (until == null) return false;
if (System.currentTimeMillis() < until) {
return true;
}
punishUntil.remove(uid);
return false;
}
// ========== 下次出题时间 ==========
private String getNextQuizTime() {
if ("手动".equals(mode)) {
return "§7(手动模式)";
}
int delay = ThreadLocalRandom.current()
.nextInt(minSec, maxSec + 1);
LocalTime next = LocalTime.now()
.plusSeconds(delay);
return next.format(
DateTimeFormatter.ofPattern("HH:mm"));
}
/**
* 多选题:统计答对几个选项
*/
private int countMultiCorrect(String input,
Question q) {
// 正确选项集合
java.util.Set<String> correct =
new java.util.HashSet<>();
for (String s : q.getAnswer().split(",")) {
correct.add(s.trim().toLowerCase());
}
// 玩家输入的选项集合
java.util.Set<String> player =
new java.util.HashSet<>();
for (String s : input.split("[,,\\s]+")) {
String clean = s.trim().toLowerCase()
.replaceAll("[()().,。,]",
"");
if (!clean.isEmpty()) {
player.add(clean);
}
}
int count = 0;
for (String c : correct) {
if (player.contains(c)) {
count++;
}
}
return count;
}
// ========== 校验 ==========
private boolean checkChoice(String input,
Question q) {
String clean = input.trim().toLowerCase()
.replaceAll("[()()\\s.,。,]", "");
String ans = q.getAnswer().trim()
.toLowerCase()
.replaceAll("[()()\\s.,。,]", "");
if (clean.equals(ans)) return true;
int ansIdx = ans.charAt(0) - 'a';
if (ansIdx >= 0
&& ansIdx < q.getOptions().size()) {
String expected = q.getOptions()
.get(ansIdx).trim().toLowerCase();
return clean.equals(expected);
}
return false;
}
private int[] checkFill(String input,
Question q) {
String[] correct =
q.getAnswer().split("[,,]");
String[] parts = input
.replaceAll("[,,\\s]+", ",")
.split(",");
int count = 0;
for (int i = 0;
i < correct.length
&& i < parts.length; i++) {
if (correct[i].trim().toLowerCase()
.equals(parts[i].trim()
.toLowerCase())) {
count++;
}
}
return new int[]{count,
q.getRewardForCount(count)};
}
// ========== 管理 ==========
public boolean isActive() {
return active;
}
public String getMode() {
return mode;
}
public int getChoiceCount() {
return choiceQ.size();
}
public int getFillCount() {
return fillQ.size();
}
public int getOpenCount() {
return openQ.size();
}
public void reload() {
stopAuto();
loadQuestions();
firstRun = true;
lastQuizTime = System.currentTimeMillis();
startAuto();
}
/**
* 随机出题(手动)
*/
public void forceQuestion() {
if (Bukkit.getOnlinePlayers().size() < 1) {
return; // 静默拦截
}
if (!active) {
lastQuizTime = System.currentTimeMillis();
broadcastQuestion();
}
}
/**
* 指定题型+编号(手动)
*/
public void forceQuestion(String type, int index) {
if (Bukkit.getOnlinePlayers().size() < 1) {
return;
}
if (!active) {
lastQuizTime = System.currentTimeMillis();
broadcastQuestion(type, index);
}
}
}
+18
View File
@@ -0,0 +1,18 @@
name: Sdf1_game
version: 1.0
main: Sdf1_game.Main
api-version: '1.21'
description: 快问快答 - 自动答题赢债券
softdepend: [Sdf1_login]
commands:
quiz:
description: 快问快答管理
usage: /quiz <reload|start|stop|test>
permission: sdf1_quiz.admin
出题:
description: 快问快答出题
usage: /出题 [选择题1]
permissions:
sdf1_quiz.admin:
default: op