feat: 新增 Sdf1_game 插件初始版本(快问块答小游戏,对接 Sdf1_login 债券系统)
This commit is contained in:
+39
@@ -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
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+7
@@ -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>
|
||||
Generated
+14
@@ -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
@@ -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>
|
||||
@@ -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 |
|
||||
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}}
|
||||
@@ -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 "问答题";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user