diff --git a/src/main/java/org/xgqy/survival/Survival.java b/src/main/java/org/xgqy/survival/Survival.java index 00305b0..02802d7 100644 --- a/src/main/java/org/xgqy/survival/Survival.java +++ b/src/main/java/org/xgqy/survival/Survival.java @@ -18,11 +18,13 @@ import org.xgqy.survival.command.InventoryCommandExecutor; import org.xgqy.survival.command.KillCommandExecutor; import org.xgqy.survival.command.LandCommandExecutor; import org.xgqy.survival.command.LoginCommandExecutor; +import org.xgqy.survival.command.MessageBoardCommandExecutor; import org.xgqy.survival.command.NoticeCommandExecutor; import org.xgqy.survival.command.PartyCommandExecutor; import org.xgqy.survival.command.PcCommandExecutor; import org.xgqy.survival.command.PointCommandExecutor; import org.xgqy.survival.command.PvpCommandExecutor; +import org.xgqy.survival.command.RankCommandExecutor; import org.xgqy.survival.command.RegCommandExecutor; import org.xgqy.survival.command.ReportCommandExecutor; import org.xgqy.survival.command.SetTagCommandExecutor; @@ -37,6 +39,7 @@ import org.xgqy.survival.event.ForceSurvival; import org.xgqy.survival.event.JoinEvent; import org.xgqy.survival.event.LandEvent; import org.xgqy.survival.event.LoginEvent; +import org.xgqy.survival.event.RankAdd; import java.io.File; import java.io.IOException; @@ -60,6 +63,7 @@ public final class Survival extends JavaPlugin { public Map banreason = new HashMap<>(); public Map> reportlist = new HashMap<>(); // Teleport + private RankAdd contributionManager; public Map teleport = new HashMap<>(); public Map Ateleport = new HashMap<>(); public Map teleportp = new HashMap<>(); @@ -151,7 +155,6 @@ public final class Survival extends JavaPlugin { // 信用分数据文件相关 private File ppointFile; private FileConfiguration ppointConfig; - @Override public void onEnable() { // 初始化账号管理器 @@ -288,15 +291,27 @@ public final class Survival extends JavaPlugin { getCommand("land").setExecutor(new LandCommandExecutor(this)); getCommand("qd").setExecutor(new LandCommandExecutor(this)); getCommand("notice").setExecutor(new NoticeCommandExecutor(this)); + MessageBoardCommandExecutor mbExecutor = new MessageBoardCommandExecutor(this); + this.getCommand("messageboard").setExecutor(mbExecutor); + this.getCommand("mb").setExecutor(mbExecutor); // 别名支持 + this.contributionManager = new RankAdd(this); + + // 注册排行榜命令 + RankCommandExecutor rankExecutor = new RankCommandExecutor(this); + this.getCommand("rank").setExecutor(rankExecutor); CommandTabExecutor tabExecutor = new CommandTabExecutor(); registerTabCompleter(tabExecutor); } + public RankAdd getContributionManager() { + return contributionManager; + } private void registerTabCompleter(CommandTabExecutor completer) { // 所有需要补全的命令列表 List commands = Arrays.asList( "handle", "tpacc", "friend", "party", "settag", "teleport", "point", "land", "pvp", "help", "tag", "hub", "home", - "qd", "reg", "login", "msg", "inventory", "notice" + "qd", "reg", "login", "msg", "inventory", "notice","messageboard","mb", + "rank" ); for (String cmd : commands) { @@ -311,6 +326,11 @@ public final class Survival extends JavaPlugin { public void onDisable() { // 保存玩家标签数据 playerTags.saveTags(); + for(Player p : Bukkit.getOnlinePlayers()){ + p.kickPlayer(ChatColor.GREEN+"========================================================\n"+ + ChatColor.RED+"服务器重启,请稍等 1 - 3 分钟后再次加入\n"+ + ChatColor.GREEN+"========================================================"); + } // 保存信用分数据 savePpoint(); diff --git a/src/main/java/org/xgqy/survival/command/CommandTabExecutor.java b/src/main/java/org/xgqy/survival/command/CommandTabExecutor.java index f90bfa2..52035a2 100644 --- a/src/main/java/org/xgqy/survival/command/CommandTabExecutor.java +++ b/src/main/java/org/xgqy/survival/command/CommandTabExecutor.java @@ -14,17 +14,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -/** - * 统一命令补全器:整合所有命令的自动补全逻辑 - */ public class CommandTabExecutor implements TabCompleter { - - // 支持的颜色列表(用于 settag 命令) private static final Map COLOR_MAP = new HashMap<>(); - static { - // 初始化颜色映射(与 SetTagCommandExecutor 对应) COLOR_MAP.put("black", ""); COLOR_MAP.put("dark_blue", ""); COLOR_MAP.put("dark_green", ""); @@ -48,8 +40,6 @@ public class CommandTabExecutor implements TabCompleter { public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { List completions = new ArrayList<>(); String cmdName = command.getName().toLowerCase(); - - // 根据命令名称分发补全逻辑 switch (cmdName) { case "handle": completions = handleTabComplete(args); @@ -86,23 +76,43 @@ public class CommandTabExecutor implements TabCompleter { case "notice": completions = simpleCommandTabComplete(sender, cmdName, args); break; + case "mb": + case "messageboard": + completions = mbTabCompelete(args); + case "rank": + completions = rankTabCompelete((Player) sender,args); default: - // 未知命令返回空 completions.clear(); break; } return completions; } - - // ------------------------------ 各命令补全逻辑 ------------------------------ - - /** - * /handle 命令补全(处理举报) - */ + private List rankTabCompelete(Player sender,String[] args){ + List completions = new ArrayList<>(); + if(sender.isOp()){ + if(args.length == 1){ + List options = Arrays.asList("add","remove","all"); + completions = filterMatches(options,args[0]); + } + else if(args.length == 2 && (args[1] == "add" || args[1] == "remove")){ + completions.add("<数值>"); + } + } + return completions; + } + private List mbTabCompelete(String[] args){ + List completions = new ArrayList<>(); + if(args.length == 1){ + List options = Arrays.asList("add","list","support"); + completions=filterMatches(options,args[0]); + }else{ + completions.add("<内容>"); + } + return completions; + } private List handleTabComplete(String[] args) { List completions = new ArrayList<>(); - // 一级参数:no/point/12h/1d/3d/7d/14d/1mo/3mo/1y/ban if (args.length == 1) { List options = Arrays.asList( "no", "point", "12h", "1d", "3d", "7d", "14d", @@ -110,107 +120,72 @@ public class CommandTabExecutor implements TabCompleter { ); completions = filterMatches(options, args[0]); } - // 二级参数:ban 后补全天数提示 else if (args.length == 2 && "ban".equalsIgnoreCase(args[0])) { completions.add("<天数>"); } return completions; } - - /** - * /tpacc 命令补全(传送申请处理) - */ private List tpAccTabComplete(String[] args) { List completions = new ArrayList<>(); - // 一级参数:accept/deny/tome/detome if (args.length == 1) { List options = Arrays.asList("accept", "deny", "tome", "detome"); completions = filterMatches(options, args[0]); } return completions; } - - /** - * /friend 命令补全(好友操作) - */ private List friendTabComplete(CommandSender sender, String[] args) { List completions = new ArrayList<>(); if (!(sender instanceof Player)) return completions; - - // 一级参数:add/allow/deny/remove/list if (args.length == 1) { List options = Arrays.asList("add", "allow", "deny", "remove", "list"); completions = filterMatches(options, args[0]); } - // 二级参数:操作后补全在线玩家名 else if (args.length == 2 && Arrays.asList("add", "allow", "deny", "remove").contains(args[0].toLowerCase())) { completions = getOnlinePlayers(args[1]); } return completions; } - - /** - * /party 命令补全(队伍操作) - */ private List partyTabComplete(CommandSender sender, String[] args) { List completions = new ArrayList<>(); if (!(sender instanceof Player)) return completions; - - // 一级参数:create/quit/join/list/kick/ban/disband/help if (args.length == 1) { List options = Arrays.asList( "create", "quit", "join", "list", "kick", "ban", "disband", "help" ); completions = filterMatches(options, args[0]); } - // 二级参数:join/kick/ban 后补全在线玩家名 else if (args.length == 2 && Arrays.asList("join", "kick", "ban").contains(args[0].toLowerCase())) { completions = getOnlinePlayers(args[1]); } return completions; } - - /** - * /settag 命令补全(设置标签) - */ private List setTagTabComplete(CommandSender sender, String[] args) { List completions = new ArrayList<>(); - // 权限校验:无权限则不显示补全 if (!sender.hasPermission("permission.settag")) return completions; - - // 一级参数:在线玩家名 if (args.length == 1) { completions = getOnlinePlayers(args[1]); } - // 二级参数:add/remove/check else if (args.length == 2) { List options = Arrays.asList("add", "remove", "check"); completions = filterMatches(options, args[1]); } - // 三级参数:add/check/remove 后补全标签内容 else if (args.length == 3) { String op = args[1].toLowerCase(); if (Arrays.asList("add", "check", "remove").contains(op)) { completions.add("<标签内容>"); } } - // 四级参数:add/check 后补全颜色 else if (args.length == 4) { String op = args[1].toLowerCase(); if (Arrays.asList("add", "check").contains(op)) { completions = filterMatches(new ArrayList<>(COLOR_MAP.keySet()), args[3]); } } - // 五级参数:add 后补全天数 else if (args.length == 5 && "add".equalsIgnoreCase(args[1])) { completions.add("<天数(999=永久)>"); } return completions; } - - /** - * /teleport 命令补全(传送) - */ private List teleportTabComplete(CommandSender sender, String[] args) { List completions = new ArrayList<>(); if (!(sender instanceof Player)) return completions; @@ -227,73 +202,48 @@ public class CommandTabExecutor implements TabCompleter { } return completions; } - - /** - * /point /land 命令补全(信用分/领地操作) - */ private List pointLandTabComplete(CommandSender sender, String[] args) { List completions = new ArrayList<>(); if (!(sender instanceof Player)) return completions; - - // 一级参数:在线玩家名 if (args.length == 1) { completions = getOnlinePlayers(args[0]); } - // 二级参数:set/add/remove else if (args.length == 2) { List options = Arrays.asList("set", "add", "remove"); completions = filterMatches(options, args[1]); } - // 三级参数:数值提示 else if (args.length == 3) { completions.add("<数值>"); } return completions; } - - /** - * 简单命令补全(pvp/help/tag/hub/selfkill/qd/reg/login/msg/inventory/notice) - */ private List simpleCommandTabComplete(CommandSender sender, String cmdName, String[] args) { List completions = new ArrayList<>(); if (!(sender instanceof Player)) return completions; - switch (cmdName) { - // /reg <密码> <确认密码> case "reg": if (args.length == 1) completions.add("<密码>"); else if (args.length == 2) completions.add("<确认密码>"); break; - // /login <密码> case "login": if (args.length == 1) completions.add("<密码>"); break; - // /msg <玩家名> <消息> case "msg": if (args.length == 1) completions = getOnlinePlayers(args[0]); else if (args.length == 2) completions.add("<消息内容>"); break; - // /inventory <玩家名> case "inventory": if (args.length == 1) completions = getOnlinePlayers(args[0]); break; - // /notice <玩家名> case "notice": if (args.length == 1) completions = getOnlinePlayers(args[0]); break; - // 无参数命令(pvp/help/tag/hub/selfkill/qd) default: completions.clear(); break; } return completions; } - - // ------------------------------ 通用工具方法 ------------------------------ - - /** - * 获取在线玩家名列表(支持前缀匹配) - */ private static List getOnlinePlayers(String partialName) { List playerNames = new ArrayList<>(); for (Player player : Bukkit.getOnlinePlayers()) { @@ -301,10 +251,6 @@ public class CommandTabExecutor implements TabCompleter { } return filterMatches(playerNames, partialName); } - - /** - * 从候选列表中过滤出匹配输入前缀的选项(不区分大小写) - */ private static List filterMatches(List candidates, String partial) { String lowerPartial = partial.toLowerCase(); return candidates.stream() diff --git a/src/main/java/org/xgqy/survival/command/HelpCommandExecutor.java b/src/main/java/org/xgqy/survival/command/HelpCommandExecutor.java index fc763be..e8b3910 100644 --- a/src/main/java/org/xgqy/survival/command/HelpCommandExecutor.java +++ b/src/main/java/org/xgqy/survival/command/HelpCommandExecutor.java @@ -28,7 +28,9 @@ public class HelpCommandExecutor implements CommandExecutor { sender.sendMessage(ChatColor.GREEN+"/party <参数> <参数> - 队伍"); sender.sendMessage(ChatColor.GREEN+"/friend <参数> <参数> - 好友"); sender.sendMessage(ChatColor.GREEN+"/msg <内容> - 私聊"); - sender.sendMessage(ChatColor.GREEN+"/selfkill - 自杀"); + sender.sendMessage(ChatColor.GREEN+"/home - 回家"); + sender.sendMessage(ChatColor.GREEN+"/rank - 榜单"); + sender.sendMessage(ChatColor.GREEN+"/mb - 留言板"); sender.sendMessage(ChatColor.YELLOW + "-----------------------------"); return true; } diff --git a/src/main/java/org/xgqy/survival/command/KillCommandExecutor.java b/src/main/java/org/xgqy/survival/command/KillCommandExecutor.java index 026af73..2c3ee29 100644 --- a/src/main/java/org/xgqy/survival/command/KillCommandExecutor.java +++ b/src/main/java/org/xgqy/survival/command/KillCommandExecutor.java @@ -4,7 +4,6 @@ import org.bukkit.ChatColor; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.Particle; -import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -87,21 +86,21 @@ public class KillCommandExecutor implements CommandExecutor, Listener { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { // 仅玩家可使用 if (!(sender instanceof Player player)) { - sender.sendMessage(ChatColor.RED + "❌ 无法对非玩家使用该命令!"); + sender.sendMessage(ChatColor.RED + "X 无法对非玩家使用该命令!"); return true; } // 1. 检查冷却状态 if (isOnCooldown(player)) { long remainingSeconds = (cooldownMap.get(player) - System.currentTimeMillis()) / 1000; - player.sendMessage(ChatColor.RED + "⚠️ 命令冷却中!剩余 " + remainingSeconds + " 秒后可再次使用"); + player.sendMessage(ChatColor.RED + "⚠ 命令冷却中!剩余 " + remainingSeconds + " 秒后可再次使用"); return true; } // 2. 检查经验是否足够 int totalExp = player.getTotalExperience(); if (totalExp < EXP_COST) { - player.sendMessage(ChatColor.RED + "❌ 经验不足!使用该命令需消耗 " + EXP_COST + " 点经验,你当前仅拥有 " + totalExp + " 点"); + player.sendMessage(ChatColor.RED + "X 经验不足!使用该命令需消耗 " + EXP_COST + " 点经验,你当前仅拥有 " + totalExp + " 点"); return true; } @@ -110,12 +109,10 @@ public class KillCommandExecutor implements CommandExecutor, Listener { Location startLoc = player.getLocation().clone(); // 记录起始位置(精确到坐标和角度) TeleportData teleportData = new TeleportData(startLoc, EXP_COST); teleportingPlayers.put(player, teleportData); - // 4. 发送传送提示 player.sendMessage(ChatColor.YELLOW + "====================================="); - player.sendMessage(ChatColor.YELLOW + "📡 自动回复活点启动!"); player.sendMessage(ChatColor.WHITE + "消耗:" + ChatColor.GREEN + EXP_COST + " 点经验"); - player.sendMessage(ChatColor.WHITE + "倒计时:" + ChatColor.RED + "5 秒(移动将取消传送)"); + player.sendMessage(ChatColor.WHITE + "倒计时:" + ChatColor.RED + "5 秒"); player.sendMessage(ChatColor.YELLOW + "====================================="); // 5. 启动粒子效果任务(每0.1秒生成一圈粒子) @@ -144,36 +141,19 @@ public class KillCommandExecutor implements CommandExecutor, Listener { * 启动传送粒子效果(围绕玩家生成圆形蓝色粒子) */ private void startParticleEffect(Player player, TeleportData data) { + int k[] = {50}; BukkitTask particleTask = new BukkitRunnable() { + @Override public void run() { // 玩家已移动或传送结束,停止粒子 if (!teleportingPlayers.containsKey(player) || data.hasMoved()) { + player.sendTitle(ChatColor.RED+"传送失败","剩余能量已退还",20,40,20); this.cancel(); return; } - - Location playerLoc = player.getLocation().add(0, PARTICLE_Y_OFFSET, 0); - World world = playerLoc.getWorld(); - if (world == null) { - this.cancel(); - return; - } - - // 生成圆形粒子(围绕玩家) - for (int i = 0; i < PARTICLE_COUNT; i++) { - // 计算圆形坐标 - double angle = 2 * Math.PI * i / PARTICLE_COUNT; - double x = Math.cos(angle) * PARTICLE_RADIUS; - double z = Math.sin(angle) * PARTICLE_RADIUS; - - Location particleLoc = playerLoc.clone().add(x, 0, z); - // 配置红色stone粒子为蓝色(更美观) - Particle.DustOptions dustOptions = new Particle.DustOptions(PARTICLE_COLOR, 1.2f); - world.spawnParticle(TELEPORT_PARTICLE, particleLoc, 0, 0, 0, 0, 0, dustOptions); - } - - playerLoc.subtract(0, PARTICLE_Y_OFFSET, 0); // 恢复玩家原位置(避免坐标偏移) + player.sendTitle(ChatColor.GREEN+"传送将在 "+String.format("%.1f",1.0*k[0]/10)+" 秒后完成",ChatColor.YELLOW+"已消耗能量: "+(String.format("%d",50-k[0])),0,20,0); + k[0] -= 1; } }.runTaskTimer(plugin, 0L, 2L); // 每2tick(0.1秒)执行一次 @@ -214,9 +194,9 @@ public class KillCommandExecutor implements CommandExecutor, Listener { } // 5. 发送传送成功提示 + player.sendTitle(ChatColor.GREEN+"传送成功",ChatColor.GREEN+"你现在处于恢复期,30秒后才能再次使用",10,50,10); player.sendMessage(ChatColor.GREEN + "====================================="); - player.sendMessage(ChatColor.GREEN + "🎉 传送成功!已返回活点"); - player.sendMessage(ChatColor.WHITE + "提示:" + ChatColor.GRAY + "30秒内无法再次使用该命令"); + player.sendMessage(ChatColor.GREEN + "传送成功!"); player.sendMessage(ChatColor.GREEN + "====================================="); // 6. 设置30秒冷却 @@ -265,16 +245,28 @@ public class KillCommandExecutor implements CommandExecutor, Listener { } // 2. 返还扣除的经验 - player.setTotalExperience(player.getTotalExperience() + data.getDeductedExp()); + //player.setTotalExperience(player.getTotalExperience() + data.getDeductedExp()); // 3. 发送取消提示 player.sendMessage(ChatColor.RED + "====================================="); player.sendMessage(ChatColor.RED + " 传送已取消!"); - player.sendMessage(ChatColor.WHITE + "原因:" + ChatColor.RED + "传送期间移动"); - player.sendMessage(ChatColor.WHITE + "提示:" + ChatColor.GRAY + "已返还 " + EXP_COST + " 点经验"); player.sendMessage(ChatColor.RED + "====================================="); // 4. 清理传送状态 teleportingPlayers.remove(player); } -} \ No newline at end of file +} +/* +[投票] +新功能投票 +1.【偏实用性】榜单 0 +2.【偏技术性】登录检测 0 +3.【偏实用性】留言板 1 +4.【偏实用性】玩家间购买 0 +5.【偏技术性】完善并添加现有功能 0 +6.【偏趣味性】打屁(限时功能)0 +7.自己提建议___________ +投票输入数字即可。(如果选择7,请附加一句建议) +结束时间: 2025-11-09 17:00 + + */ \ No newline at end of file diff --git a/src/main/java/org/xgqy/survival/command/MessageBoardCommandExecutor.java b/src/main/java/org/xgqy/survival/command/MessageBoardCommandExecutor.java new file mode 100644 index 0000000..12696fd --- /dev/null +++ b/src/main/java/org/xgqy/survival/command/MessageBoardCommandExecutor.java @@ -0,0 +1,448 @@ +package org.xgqy.survival.command; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.xgqy.survival.Survival; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MessageBoardCommandExecutor implements CommandExecutor { + + // 核心配置常量 + private static final long COOLDOWN_DURATION = 24 * 60 * 60 * 1000; // 1天冷却(毫秒) + private static final int MAX_MESSAGE_LENGTH = 20; // 最大纯文本长度(不含颜色代码) + private static final int MESSAGES_PER_PAGE = 10; // 每页留言数 + + // 依赖对象 + private final Survival plugin; + private final Gson gson; + private final File messagesFile; + private final File cooldownsFile; + + // 内存数据缓存 + private List messages; + private Map playerCooldowns; + + // 留言类型枚举(区分普通留言和上报信息) + public enum MessageType { + ADD, SUPPORT + } + + // 留言实体类(Gson序列化用) + public static class Message { + private String content; // 带颜色的内容 + private String playerName; // 玩家名称 + private String playerUuid; // 玩家UUID(防改名) + private long timestamp; // 时间戳 + private MessageType type; // 留言类型 + + // Gson必需的默认构造函数 + public Message() {} + + public Message(String content, String playerName, String playerUuid, long timestamp, MessageType type) { + this.content = content; + this.playerName = playerName; + this.playerUuid = playerUuid; + this.timestamp = timestamp; + this.type = type; + } + + // Getter和Setter(Gson序列化必需) + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + public String getPlayerName() { return playerName; } + public void setPlayerName(String playerName) { this.playerName = playerName; } + public String getPlayerUuid() { return playerUuid; } + public void setPlayerUuid(String playerUuid) { this.playerUuid = playerUuid; } + public long getTimestamp() { return timestamp; } + public void setTimestamp(long timestamp) { this.timestamp = timestamp; } + public MessageType getType() { return type; } + public void setType(MessageType type) { this.type = type; } + } + + // 玩家冷却实体类 + public static class PlayerCooldown { + private String playerUuid; + private long lastAddTime; // 最后留言时间 + private long lastSupportTime; // 最后上报时间 + + public PlayerCooldown() {} + + public PlayerCooldown(String playerUuid, long lastAddTime, long lastSupportTime) { + this.playerUuid = playerUuid; + this.lastAddTime = lastAddTime; + this.lastSupportTime = lastSupportTime; + } + + // Getter和Setter + public String getPlayerUuid() { return playerUuid; } + public void setPlayerUuid(String playerUuid) { this.playerUuid = playerUuid; } + public long getLastAddTime() { return lastAddTime; } + public void setLastAddTime(long lastAddTime) { this.lastAddTime = lastAddTime; } + public long getLastSupportTime() { return lastSupportTime; } + public void setLastSupportTime(long lastSupportTime) { this.lastSupportTime = lastSupportTime; } + } + + public MessageBoardCommandExecutor(Survival plugin) { + this.plugin = plugin; + this.gson = new GsonBuilder().setPrettyPrinting().create(); + + // 初始化数据文件(存放在插件data目录) + this.messagesFile = new File(plugin.getDataFolder(), "messages.json"); + this.cooldownsFile = new File(plugin.getDataFolder(), "playerCooldowns.json"); + + // 加载数据(文件不存在则自动创建) + this.messages = loadMessages(); + this.playerCooldowns = loadPlayerCooldowns(); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + // 仅允许玩家使用 + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatColor.RED + "❌ 仅玩家可使用该命令!"); + return true; + } + + // 无参数时显示帮助和颜色提示 + if (args.length == 0) { + sendHelpMessage(player); + sendColorCodeTips(player); + return true; + } + + // 处理子命令 + String subCommand = args[0].toLowerCase(); + switch (subCommand) { + case "add" -> handleAddCommand(player, args); + case "list" -> handleListCommand(player, args); + case "support" -> handleSupportCommand(player, args); + default -> { + player.sendMessage(ChatColor.RED + "❌ 未知子命令!可用:add、list、support"); + sendHelpMessage(player); + } + } + + return true; + } + + /** + * 处理添加留言命令 /messageboard add <留言> + */ + private void handleAddCommand(Player player, String[] args) { + // 检查参数完整性 + if (args.length < 2) { + player.sendMessage(ChatColor.RED + "❌ 用法错误!正确格式:/messageboard add <留言内容>"); + return; + } + + // 拼接留言内容(支持空格) + StringBuilder contentSb = new StringBuilder(); + for (int i = 1; i < args.length; i++) { + contentSb.append(args[i]).append(" "); + } + String rawContent = contentSb.toString().trim(); + + // 处理颜色代码(&替换为§) + String coloredContent = ChatColor.translateAlternateColorCodes('&', rawContent); + + // 检查纯文本长度(排除颜色代码) + int plainLength = getPlainTextLength(rawContent); + if (plainLength > MAX_MESSAGE_LENGTH) { + player.sendMessage(ChatColor.RED + "❌ 留言过长!纯文本(不含颜色代码)最多" + MAX_MESSAGE_LENGTH + "字符,当前:" + plainLength); + return; + } + + // 检查冷却时间 + PlayerCooldown cooldown = getOrCreateCooldown(player); + long currentTime = System.currentTimeMillis(); + long remainingTime = cooldown.getLastAddTime() + COOLDOWN_DURATION - currentTime; + + if (remainingTime > 0) { + player.sendMessage(ChatColor.RED + "❌ 今日留言次数已用尽!请等待" + formatTime(remainingTime) + "后再试"); + return; + } + + // 保存留言 + Message message = new Message( + coloredContent, + player.getName(), + player.getUniqueId().toString(), + currentTime, + MessageType.ADD + ); + messages.add(message); + saveMessages(); + + // 更新冷却时间 + cooldown.setLastAddTime(currentTime); + savePlayerCooldowns(); + + // 反馈成功 + player.sendMessage(ChatColor.GREEN + "✅ 留言发布成功!"); + player.sendMessage(ChatColor.WHITE + "内容:" + coloredContent); + } + + /** + * 处理上报命令 /messageboard support <上报信息> + */ + private void handleSupportCommand(Player player, String[] args) { + // 检查参数完整性 + if (args.length < 2) { + player.sendMessage(ChatColor.RED + "❌ 用法错误!正确格式:/messageboard support <上报信息>"); + return; + } + + // 拼接上报内容 + StringBuilder contentSb = new StringBuilder(); + for (int i = 1; i < args.length; i++) { + contentSb.append(args[i]).append(" "); + } + String rawContent = contentSb.toString().trim(); + + // 处理颜色代码 + String coloredContent = ChatColor.translateAlternateColorCodes('&', rawContent); + + // 检查冷却时间 + PlayerCooldown cooldown = getOrCreateCooldown(player); + long currentTime = System.currentTimeMillis(); + long remainingTime = cooldown.getLastSupportTime() + COOLDOWN_DURATION - currentTime; + + if (remainingTime > 0) { + player.sendMessage(ChatColor.RED + "❌ 今日上报次数已用尽!请等待" + formatTime(remainingTime) + "后再试"); + return; + } + + // 保存上报信息 + Message message = new Message( + coloredContent, + player.getName(), + player.getUniqueId().toString(), + currentTime, + MessageType.SUPPORT + ); + messages.add(message); + saveMessages(); + + // 更新冷却时间 + cooldown.setLastSupportTime(currentTime); + savePlayerCooldowns(); + + // 反馈成功 + player.sendMessage(ChatColor.GREEN + "✅ 上报成功!管理员会尽快处理"); + player.sendMessage(ChatColor.WHITE + "上报内容:" + coloredContent); + } + + /** + * 处理查看列表命令 /messageboard list [页码] + */ + private void handleListCommand(Player player, String[] args) { + // 筛选可见留言(普通玩家仅看ADD,管理员看所有) + boolean isAdmin = player.hasPermission("messageboard.admin") || player.isOp(); + List visibleMessages = new ArrayList<>(); + for (Message msg : messages) { + if (isAdmin || msg.getType() == MessageType.ADD) { + visibleMessages.add(msg); + } + } + + // 按时间倒序排序(最新在前) + visibleMessages.sort(Comparator.comparingLong(Message::getTimestamp).reversed()); + + // 解析页码(默认第1页) + int page = 1; + if (args.length >= 2) { + try { + page = Integer.parseInt(args[1]); + if (page < 1) page = 1; + } catch (NumberFormatException e) { + player.sendMessage(ChatColor.RED + "❌ 页码必须是正整数!默认显示第1页"); + } + } + + // 分页计算 + int totalMsg = visibleMessages.size(); + int totalPages = (totalMsg + MESSAGES_PER_PAGE - 1) / MESSAGES_PER_PAGE; // 向上取整 + + // 检查页码有效性 + if (page > totalPages) { + player.sendMessage(ChatColor.RED + "❌ 页码超出范围!总页数:" + totalPages); + return; + } + + // 截取当前页数据 + int start = (page - 1) * MESSAGES_PER_PAGE; + int end = Math.min(start + MESSAGES_PER_PAGE, totalMsg); + List pageMessages = visibleMessages.subList(start, end); + + // 发送列表信息 + player.sendMessage(ChatColor.GREEN + "===== 留言列表(第" + page + "/" + totalPages + "页)====="); + if (pageMessages.isEmpty()) { + player.sendMessage(ChatColor.GRAY + "📭 当前页暂无留言"); + } else { + for (int i = 0; i < pageMessages.size(); i++) { + Message msg = pageMessages.get(i); + String typeTag = msg.getType() == MessageType.SUPPORT ? ChatColor.RED + "[上报] " : ""; + String line = ChatColor.WHITE + String.format("%d",(start + i + 1)) + ". " + typeTag + msg.getContent() + + ChatColor.GRAY + " - " + msg.getPlayerName(); + player.sendMessage(line); + } + } + player.sendMessage(ChatColor.GREEN + "=========================="); + player.sendMessage(ChatColor.YELLOW + "💡 提示:输入 /messageboard list <页码> 查看其他页"); + } + + /** + * 发送帮助信息 + */ + private void sendHelpMessage(Player player) { + player.sendMessage(ChatColor.GREEN + "===== 留言板命令帮助 ====="); + player.sendMessage(ChatColor.WHITE + "/mb add <留言> - 发布公开留言(1天1次,纯文本≤20字符)"); + player.sendMessage(ChatColor.WHITE + "/mb list [页码] - 查看留言列表(1页10条)"); + player.sendMessage(ChatColor.WHITE + "/mb support <信息> - 上报问题(1天1次,仅管理员可见)"); + player.sendMessage(ChatColor.GREEN + "=========================="); + } + + /** + * 发送颜色代码使用提示 + */ + private void sendColorCodeTips(Player player) { + player.sendMessage(ChatColor.GREEN + "===== 可用颜色代码 ====="); + player.sendMessage(ChatColor.BLACK + "&0 黑色 " + ChatColor.DARK_BLUE + "&1 深蓝色 " + ChatColor.DARK_GREEN + "&2 深绿色"); + player.sendMessage(ChatColor.DARK_AQUA + "&3 深青色 " + ChatColor.DARK_RED + "&4 深红色 " + ChatColor.DARK_PURPLE + "&5 深紫色"); + player.sendMessage(ChatColor.GOLD + "&6 金色 " + ChatColor.GRAY + "&7 灰色 " + ChatColor.DARK_GRAY + "&8 深灰色"); + player.sendMessage(ChatColor.BLUE + "&9 蓝色 " + ChatColor.GREEN + "&a 绿色 " + ChatColor.AQUA + "&b 青色"); + player.sendMessage(ChatColor.RED + "&c 红色 " + ChatColor.LIGHT_PURPLE + "&d 紫色 " + ChatColor.YELLOW + "&e 黄色"); + player.sendMessage(ChatColor.WHITE + "&f 白色"); + player.sendMessage(ChatColor.GREEN + "======================="); + player.sendMessage(ChatColor.YELLOW + "💡 提示:颜色代码(&+字符)不计入20字符限制"); + } + + /** + * 计算纯文本长度(移除所有颜色代码) + */ + private int getPlainTextLength(String rawContent) { + // 正则替换所有 & 后跟一个字符(颜色代码)为空白 + return rawContent.replaceAll("&.", "").length(); + } + + /** + * 格式化时间(毫秒转天时分秒) + */ + private String formatTime(long ms) { + long days = ms / (24 * 60 * 60 * 1000); + ms %= 24 * 60 * 60 * 1000; + long hours = ms / (60 * 60 * 1000); + ms %= 60 * 60 * 1000; + long minutes = ms / (60 * 1000); + long seconds = ms / 1000; + + StringBuilder sb = new StringBuilder(); + if (days > 0) sb.append(days).append("天"); + if (hours > 0) sb.append(hours).append("时"); + if (minutes > 0) sb.append(minutes).append("分"); + if (seconds > 0) sb.append(seconds).append("秒"); + return sb.toString().isEmpty() ? "0秒" : sb.toString(); + } + + /** + * 获取或创建玩家冷却数据 + */ + private PlayerCooldown getOrCreateCooldown(Player player) { + String uuid = player.getUniqueId().toString(); + return playerCooldowns.computeIfAbsent(uuid, k -> new PlayerCooldown(uuid, 0, 0)); + } + + /** + * 加载留言数据(从JSON文件) + */ + private List loadMessages() { + if (!messagesFile.exists()) { + createFile(messagesFile); + return new ArrayList<>(); + } + + try (FileReader reader = new FileReader(messagesFile)) { + Type listType = new TypeToken>() {}.getType(); + List loaded = gson.fromJson(reader, listType); + return loaded != null ? loaded : new ArrayList<>(); + } catch (IOException e) { + plugin.getLogger().severe("加载留言文件失败:" + e.getMessage()); + e.printStackTrace(); + return new ArrayList<>(); + } + } + + /** + * 保存留言数据(到JSON文件) + */ + private void saveMessages() { + try (FileWriter writer = new FileWriter(messagesFile)) { + gson.toJson(messages, writer); + } catch (IOException e) { + plugin.getLogger().severe("保存留言文件失败:" + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 加载玩家冷却数据 + */ + private Map loadPlayerCooldowns() { + if (!cooldownsFile.exists()) { + createFile(cooldownsFile); + return new HashMap<>(); + } + + try (FileReader reader = new FileReader(cooldownsFile)) { + Type mapType = new TypeToken>() {}.getType(); + Map loaded = gson.fromJson(reader, mapType); + return loaded != null ? loaded : new HashMap<>(); + } catch (IOException e) { + plugin.getLogger().severe("加载冷却文件失败:" + e.getMessage()); + e.printStackTrace(); + return new HashMap<>(); + } + } + + /** + * 保存玩家冷却数据 + */ + private void savePlayerCooldowns() { + try (FileWriter writer = new FileWriter(cooldownsFile)) { + gson.toJson(playerCooldowns, writer); + } catch (IOException e) { + plugin.getLogger().severe("保存冷却文件失败:" + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 创建文件(自动创建父目录) + */ + private void createFile(File file) { + try { + file.getParentFile().mkdirs(); // 创建父目录 + file.createNewFile(); // 创建文件 + } catch (IOException e) { + plugin.getLogger().severe("创建文件失败:" + file.getPath() + " - " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/xgqy/survival/command/RankCommandExecutor.java b/src/main/java/org/xgqy/survival/command/RankCommandExecutor.java new file mode 100644 index 0000000..8000b97 --- /dev/null +++ b/src/main/java/org/xgqy/survival/command/RankCommandExecutor.java @@ -0,0 +1,216 @@ +package org.xgqy.survival.command; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.xgqy.survival.Survival; +import org.xgqy.survival.event.RankAdd; + +import java.util.List; + +public class RankCommandExecutor implements CommandExecutor { + + private final Survival plugin; + // 关键修正1:成员变量类型改为 ContributionManager(之前错写为 RankAdd) + private final RankAdd contributionManager; + private static final String ADMIN_PERMISSION = "rank.admin"; + private static final int TOP_LIMIT = 10; + + public RankCommandExecutor(Survival plugin) { + this.plugin = plugin; + // 关键修正2:获取正确的 ContributionManager 实例(依赖主类的 getContributionManager 方法) + this.contributionManager = plugin.getContributionManager(); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + sendTop10Rank(sender); + return true; + } + + String subCommand = args[0].toLowerCase(); + switch (subCommand) { + case "add": + handleAddScore(sender, args); + break; + case "remove": + handleRemoveScore(sender, args); + break; + case "all": + sendAllRank(sender); + break; + default: + sender.sendMessage(ChatColor.RED + "未知子命令!可用:add、remove、all"); + sendHelpMessage(sender); + } + return true; + } + private void sendTop10Rank(CommandSender sender) { + sender.sendMessage(ChatColor.GOLD + "===== 贡献值排行榜(前10名)====="); + // 关键修正3:使用 ContributionManager 的正确内部类 PlayerContribution + List topList = contributionManager.getTopContributors(); + + for (int i = 0; i < TOP_LIMIT; i++) { + int rank = i + 1; + if (i < topList.size()) { + RankAdd.PlayerContribution data = topList.get(i); + String rankPrefix = switch (rank) { + case 1 -> ChatColor.RED + "🥇 " + ChatColor.WHITE; + case 2 -> ChatColor.GRAY + "🥈 " + ChatColor.WHITE; + case 3 -> ChatColor.YELLOW + "🥉 " + ChatColor.WHITE; + default -> ChatColor.WHITE + "[" + rank + "] "; + }; + // 修正4:现在能正确识别 getPlayerName() 和 getTotalContribution()(因为 data 是正确的类型) + String line = rankPrefix + data.getPlayerName() + ChatColor.GRAY + " - " + ChatColor.YELLOW + data.getTotalContribution() + "点"; + sender.sendMessage(line); + } else { + sender.sendMessage(ChatColor.WHITE + "[" + rank + "] " + ChatColor.GRAY + "暂无 - 暂无"); + } + } + sender.sendMessage(ChatColor.GOLD + "=============================="); + if (sender.hasPermission(ADMIN_PERMISSION)) { + sender.sendMessage(ChatColor.YELLOW + "管理员提示:使用 /rank all 查看完整列表"); + } + } + + /** + * 发送所有玩家贡献值完整列表 + */ + private void sendAllRank(CommandSender sender) { + if (!sender.hasPermission(ADMIN_PERMISSION)) { + sender.sendMessage(ChatColor.RED + "无权限使用该命令!仅管理员可查看完整列表"); + return; + } + + sender.sendMessage(ChatColor.GOLD + "===== 全服贡献值完整列表 ====="); + List allList = contributionManager.getAllContributions(); + + if (allList.isEmpty()) { + sender.sendMessage(ChatColor.GRAY + "暂无任何玩家贡献值数据"); + } else { + for (int i = 0; i < allList.size(); i++) { + RankAdd.PlayerContribution data = allList.get(i); + int rank = i + 1; + String line = ChatColor.WHITE + "[" + rank + "] " + data.getPlayerName() + + ChatColor.GRAY + " - " + ChatColor.YELLOW + data.getTotalContribution() + "点"; + sender.sendMessage(line); + } + } + sender.sendMessage(ChatColor.GOLD + "=============================="); + sender.sendMessage(ChatColor.YELLOW + "总计 " + allList.size() + " 名玩家有贡献值记录"); + } + + /** + * 管理员添加玩家贡献值 + */ + private void handleAddScore(CommandSender sender, String[] args) { + if (!sender.hasPermission(ADMIN_PERMISSION)) { + sender.sendMessage(ChatColor.RED + "无权限使用该命令!仅管理员可操作"); + return; + } + + if (args.length < 3) { + sender.sendMessage(ChatColor.RED + "用法错误!正确格式:/rank add <玩家名> <分数>"); + return; + } + + String playerName = args[1]; + int score; + try { + score = Integer.parseInt(args[2]); + if (score <= 0) { + sender.sendMessage(ChatColor.RED + "分数必须为正整数!"); + return; + } + } catch (NumberFormatException e) { + sender.sendMessage(ChatColor.RED + "分数必须是整数!"); + return; + } + + OfflinePlayer targetPlayer = Bukkit.getOfflinePlayer(playerName); + if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline()) { + sender.sendMessage(ChatColor.RED + "未找到玩家 " + playerName + "(从未加入过服务器)"); + return; + } + + String reason = "管理员[" + sender.getName() + "]手动添加分数"; + // 修正5:调用 RankAdd 的正确方法,内部类枚举也需正确引用 + int newScore = contributionManager.addCustomContribution( + targetPlayer.getPlayer(), + score, + RankAdd.ContributionType.TASK_MAIN, // 正确引用枚举 + reason + ); + + sender.sendMessage(ChatColor.GREEN + "操作成功!"); + sender.sendMessage(ChatColor.WHITE + "玩家:" + targetPlayer.getName()); + sender.sendMessage(ChatColor.WHITE + "添加分数:" + ChatColor.YELLOW + score + "点"); + sender.sendMessage(ChatColor.WHITE + "当前总贡献:" + ChatColor.YELLOW + newScore + "点"); + } + + /** + * 管理员减少玩家贡献值 + */ + private void handleRemoveScore(CommandSender sender, String[] args) { + if (!sender.hasPermission(ADMIN_PERMISSION)) { + sender.sendMessage(ChatColor.RED + "无权限使用该命令!仅管理员可操作"); + return; + } + + if (args.length < 3) { + sender.sendMessage(ChatColor.RED + "用法错误!正确格式:/rank remove <玩家名> <分数>"); + return; + } + + String playerName = args[1]; + int score; + try { + score = Integer.parseInt(args[2]); + if (score <= 0) { + sender.sendMessage(ChatColor.RED + "分数必须为正整数!"); + return; + } + } catch (NumberFormatException e) { + sender.sendMessage(ChatColor.RED + "分数必须是整数!"); + return; + } + + OfflinePlayer targetPlayer = Bukkit.getOfflinePlayer(playerName); + if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline()) { + sender.sendMessage(ChatColor.RED + "未找到玩家 " + playerName + "(从未加入过服务器)"); + return; + } + + String reason = "管理员[" + sender.getName() + "]手动扣除分数"; + int newScore = contributionManager.reduceContribution( + targetPlayer.getPlayer(), + score, + RankAdd.ContributionType.TASK_MAIN, + reason + ); + + sender.sendMessage(ChatColor.GREEN + "操作成功!"); + sender.sendMessage(ChatColor.WHITE + "玩家:" + targetPlayer.getName()); + sender.sendMessage(ChatColor.WHITE + "扣除分数:" + ChatColor.RED + score + "点"); + sender.sendMessage(ChatColor.WHITE + "当前总贡献:" + ChatColor.YELLOW + newScore + "点"); + } + + /** + * 发送帮助信息 + */ + private void sendHelpMessage(CommandSender sender) { + sender.sendMessage(ChatColor.GREEN + "===== 贡献值排行榜命令帮助 ====="); + sender.sendMessage(ChatColor.WHITE + "/rank - 查看前10名贡献值排行榜"); + if (sender.hasPermission(ADMIN_PERMISSION)) { + sender.sendMessage(ChatColor.WHITE + "/rank add <玩家名> <分数> - 为玩家添加贡献值"); + sender.sendMessage(ChatColor.WHITE + "/rank remove <玩家名> <分数> - 为玩家扣除贡献值"); + sender.sendMessage(ChatColor.WHITE + "/rank all - 查看全服玩家贡献值完整列表"); + } + sender.sendMessage(ChatColor.GREEN + "=============================="); + } +} \ No newline at end of file diff --git a/src/main/java/org/xgqy/survival/event/FartExecutor.java b/src/main/java/org/xgqy/survival/event/FartExecutor.java new file mode 100644 index 0000000..7e3f5e4 --- /dev/null +++ b/src/main/java/org/xgqy/survival/event/FartExecutor.java @@ -0,0 +1,7 @@ +package org.xgqy.survival.event; + +import org.bukkit.event.Listener; + +public class FartExecutor implements Listener { + +} diff --git a/src/main/java/org/xgqy/survival/event/ForceSurvival.java b/src/main/java/org/xgqy/survival/event/ForceSurvival.java index f934101..b6d5476 100644 --- a/src/main/java/org/xgqy/survival/event/ForceSurvival.java +++ b/src/main/java/org/xgqy/survival/event/ForceSurvival.java @@ -11,6 +11,7 @@ public class ForceSurvival implements Listener { private void gamemodechange(PlayerGameModeChangeEvent event){ if(event.getNewGameMode() != GameMode.SURVIVAL){ event.getPlayer().setGameMode(GameMode.SURVIVAL); + event.setCancelled(true); event.getPlayer().sendMessage(ChatColor.AQUA+"星阁钱语 "+ChatColor.WHITE+"| "+ChatColor.RED+"您不被允许切换游戏模式"); } } diff --git a/src/main/java/org/xgqy/survival/event/LoginEvent.java b/src/main/java/org/xgqy/survival/event/LoginEvent.java index a210393..fc94937 100644 --- a/src/main/java/org/xgqy/survival/event/LoginEvent.java +++ b/src/main/java/org/xgqy/survival/event/LoginEvent.java @@ -13,6 +13,8 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; @@ -35,6 +37,11 @@ public class LoginEvent implements Listener { private final Survival plugin; // 存储未登录玩家的初始位置(登录后恢复用) private final Map initialLocations = new HashMap<>(); + // 记录未登录玩家是否已落地(用于死亡保护逻辑) + private final Map unloggedPlayerLanded = new HashMap<>(); + // 落地判定参数(可根据服务器世界设置调整) + private static final double LAND_Y_THRESHOLD = 64; // 落地Y轴阈值(默认世界表面平均高度) + private static final int HIGH_ALTITUDE_SAFE = 200; // 高危高空判定(超过此高度强制使用重生点) public LoginEvent(Survival plugin) { this.plugin = plugin; @@ -45,13 +52,17 @@ public class LoginEvent implements Listener { Player player = event.getPlayer(); Location initialLoc = player.getLocation().clone(); initialLocations.put(player, initialLoc); // 记录初始位置 + unloggedPlayerLanded.put(player, false); // 初始化未落地状态 // 初始化未登录隔离状态:隐身+高处传送+致盲 setupUnloggedState(player); + + // 新玩家自动添加"小萌新"标签(30天) if(!plugin.getAccountManager().isRegistered(player.getUniqueId())){ if(!plugin.getPlayerTags().getTags(player).contains("小萌新")){ plugin.getPlayerTags().addTag(player,"小萌新",30); } } + // 登录提示定时器(每秒发送一次,登录后停止) new BukkitRunnable() { @Override @@ -59,32 +70,27 @@ public class LoginEvent implements Listener { if (plugin.getAccountManager().isLoggedIn(player)) { // 登录成功,恢复正常状态 restoreLoggedState(player); - initialLocations.remove(player); // 移除缓存的初始位置 + initialLocations.remove(player); + unloggedPlayerLanded.remove(player); this.cancel(); return; } - // 发送登录/注册提示 + // 发送登录/注册提示(修复注册命令格式错误) if (plugin.getAccountManager().isRegistered(player.getUniqueId())) { - player.sendMessage("§e欢迎回来! 请使用 /login <密码> 登录"); + player.sendMessage("§e欢迎回来! 请使用 §a/login <密码> §e登录"); player.sendTitle("§b星阁钱语", "§e请输入 /login <密码> 登录", 0, 100, 0); } else { - BaseComponent[] message = new ComponentBuilder() - .append(new ComponentBuilder(ChatColor.GREEN + "请先同意 《星阁钱语服务条款》(点击复制链接)") - // 设置点击事件:复制链接到剪贴板 - .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "https://starpavilion.xyz/terms.html")) - // 设置悬浮事件:鼠标悬停时显示的文字 - .event(new HoverEvent( - HoverEvent.Action.SHOW_TEXT, // 悬浮动作:显示文本 - // 悬浮文字内容(可带颜色) - new ComponentBuilder() - .append(ChatColor.GRAY+"https://starpavilion.xyz/terms.html") - .create() - )) - .create()) + BaseComponent[] termsMsg = new ComponentBuilder() + .append(ChatColor.GREEN + "请先同意 《星阁钱语服务条款》(点击复制链接)") + .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "https://starpavilion.xyz/terms.html")) + .event(new HoverEvent( + HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder(ChatColor.GRAY + "https://starpavilion.xyz/terms.html").create() + )) .create(); - player.spigot().sendMessage(message); - player.sendMessage("§e欢迎来到服务器! 请使用 /reg <密码> <确认密码> 注册"); - player.sendTitle("§b星阁钱语", "§e请输入 /reg <密码> <密码> <确认码> 注册", 0, 100, 0); + player.spigot().sendMessage(termsMsg); + player.sendMessage("§e欢迎来到服务器! 请使用 §a/reg <密码> <确认密码> <确认码> §e注册"); + player.sendTitle("§b星阁钱语", "§e请输入 /reg <密码> <确认密码> <确认码>", 0, 100, 0); } } }.runTaskTimer(plugin, 60, 60); // 1秒发送一次 @@ -95,7 +101,8 @@ public class LoginEvent implements Listener { public void run() { if (!plugin.getAccountManager().isLoggedIn(player)) { initialLocations.remove(player); - player.kickPlayer("§c登录超时"); + unloggedPlayerLanded.remove(player); + player.kickPlayer("§c登录超时(60秒未操作)"); } } }.runTaskLater(plugin, 20 * 60); @@ -107,14 +114,37 @@ public class LoginEvent implements Listener { // 玩家退出时登出账号并清理缓存 plugin.getAccountManager().logout(player); initialLocations.remove(player); + unloggedPlayerLanded.remove(player); } // ------------------------------ 核心拦截:阻止所有外界交互 ------------------------------ @EventHandler public void onPlayerMove(PlayerMoveEvent event) { - if (!plugin.getAccountManager().isLoggedIn(event.getPlayer())) { - // 完全阻止移动(包括位置和视角旋转) - event.setCancelled(true); + Player player = event.getPlayer(); + if (!plugin.getAccountManager().isLoggedIn(player)) { + Location from = event.getFrom(); + Location to = event.getTo(); + if (to == null) { + event.setCancelled(true); + return; + } + + // 允许垂直下落(保留Y轴移动),阻止水平移动(X/Z轴固定)和视角旋转(Yaw/Pitch固定) + Location newLoc = new Location( + player.getWorld(), + from.getX(), // 固定X轴 + to.getY(), // 允许Y轴下落 + from.getZ(), // 固定Z轴 + from.getYaw(), // 固定视角水平旋转 + from.getPitch() // 固定视角垂直旋转 + ); + event.setTo(newLoc); + + // 落地检测:Y轴低于阈值 + 脚下有实体方块 + if (newLoc.getY() <= LAND_Y_THRESHOLD && isBlockBelow(player)) { + unloggedPlayerLanded.put(player, true); + player.sendMessage("§a已落地!请尽快登录或注册"); + } } } @@ -122,20 +152,20 @@ public class LoginEvent implements Listener { public void onPlayerChat(AsyncPlayerChatEvent event) { if (!plugin.getAccountManager().isLoggedIn(event.getPlayer())) { event.setCancelled(true); - event.getPlayer().sendMessage("§c请先登录或注册"); + event.getPlayer().sendMessage("§c请先登录或注册后再聊天"); } } @EventHandler public void onPlayerCommand(PlayerCommandPreprocessEvent event) { - String command = event.getMessage().toLowerCase(); - // 仅允许登录/注册命令 - if (command.startsWith("/reg") || command.startsWith("/login")) { + String command = event.getMessage().toLowerCase().trim(); + // 仅允许登录/注册命令(支持带参数的完整命令) + if (command.startsWith("/reg ") || command.startsWith("/login ")) { return; } if (!plugin.getAccountManager().isLoggedIn(event.getPlayer())) { event.setCancelled(true); - event.getPlayer().sendMessage("§c请先登录或注册"); + event.getPlayer().sendMessage("§c请先登录或注册(仅允许 /reg /login 命令)"); } } @@ -203,18 +233,64 @@ public class LoginEvent implements Listener { } } - // ------------------------------ 状态管理工具方法 ------------------------------ + // ------------------------------ 死亡保护:未落地前阻止伤害和死亡 ------------------------------ + /** + * 拦截未登录玩家的伤害(主要是摔落伤害) + */ + @EventHandler + public void onEntityDamage(EntityDamageEvent event) { + if (event.getEntity() instanceof Player player) { + // 未登录且未落地时,拦截所有伤害并恢复生命值 + if (!plugin.getAccountManager().isLoggedIn(player) && !unloggedPlayerLanded.getOrDefault(player, false)) { + event.setCancelled(true); + // 强制恢复满生命值和饥饿值 + player.setHealth(player.getMaxHealth()); + player.setFoodLevel(20); + player.setSaturation(20); + } + } + } + + /** + * 拦截未登录玩家的死亡事件 + */ + @EventHandler + public void onPlayerDeath(PlayerDeathEvent event) { + Player player = event.getEntity(); + if (!plugin.getAccountManager().isLoggedIn(player)) { + event.setDeathMessage("被资本做局了"); + // 恢复状态,避免死亡惩罚 + player.setHealth(player.getMaxHealth()); + player.setFoodLevel(20); + player.setSaturation(20); + player.getActivePotionEffects().forEach(effect -> + player.removePotionEffect(effect.getType()) + ); + player.sendMessage("§c未登录状态下无法死亡,请尽快登录或注册"); + } + } + + // ------------------------------ 工具方法 ------------------------------ + /** + * 检测玩家脚下是否有实体方块(落地判定辅助) + */ + private boolean isBlockBelow(Player player) { + Location below = player.getLocation().subtract(0, 1, 0); + return below.getBlock().getType().isSolid() && !below.getBlock().getType().isAir(); + } + /** * 设置未登录玩家的隔离状态:隐身+高处传送+致盲+交互阻断 */ private void setupUnloggedState(Player player) { // 1. 隐身(对其他玩家不可见) player.setInvisible(true); - // 2. 隐藏玩家列表名称(不在Tab列表显示) + // 2. 隐藏Tab列表名称 player.setPlayerListName(""); - // 3. 禁用飞行控制(防止移动) + // 3. 禁用飞行(防止异常移动) player.setAllowFlight(false); - // 4. 传送至当前世界的安全高处(Y=255,避免掉落后受伤) + player.setFlying(false); + // 4. 传送至当前世界高空(Y=255,确保垂直下落路径安全) Location highLoc = new Location( player.getWorld(), player.getLocation().getX(), @@ -222,47 +298,88 @@ public class LoginEvent implements Listener { player.getLocation().getZ() ); player.teleport(highLoc); - // 6. 移除所有可能的干扰效果(保留致盲) + // 5. 施加致盲效果(增强隔离体验) + player.addPotionEffect(PotionEffectType.BLINDNESS.createEffect(Integer.MAX_VALUE, 0)); + // 6. 移除所有干扰效果(保留致盲) player.getActivePotionEffects().forEach(effect -> { if (effect.getType() != PotionEffectType.BLINDNESS) { player.removePotionEffect(effect.getType()); } }); + // 7. 恢复满状态 + player.setHealth(player.getMaxHealth()); + player.setFoodLevel(20); } - /** - * 恢复登录玩家的正常状态 - */ + /** * 恢复登录玩家的正常状态 */ public void restoreLoggedState(Player player) { - // 1. 先设置更长时间的无敌(10秒=200tick),确保覆盖坠落过程 - player.setNoDamageTicks(200); - + // 1. 清理未登录状态缓存 + unloggedPlayerLanded.remove(player); // 2. 恢复可见性 player.setInvisible(false); - - // 3. 恢复玩家列表名称 + // 3. 恢复Tab列表名称(带标签) List tags = plugin.getPlayerTags().getTags(player); int currentTagIndex = plugin.getPlayerTags().getCurrentTag(player); + // 安全处理标签索引(防止数组越界) int validIndex = Math.max(0, Math.min(currentTagIndex, tags.size() - 1)); - player.setPlayerListName(ChatColor.WHITE + "[" + tags.get(validIndex) + ChatColor.WHITE + "]" + player.getName()); - - // 4. 恢复飞行权限 + String tag = tags.isEmpty() ? "默认" : tags.get(validIndex); + player.setPlayerListName(ChatColor.WHITE + "[" + tag + ChatColor.WHITE + "]" + player.getName()); + // 4. 恢复飞行权限(默认禁用,根据服务器配置调整) player.setAllowFlight(false); - + player.setFlying(false); // 5. 移除致盲效果 player.removePotionEffect(PotionEffectType.BLINDNESS); - - // 6. 传送回初始位置(优先使用安全的重生点) + // 6. 传送回安全初始位置(避免高空危险) Location initialLoc = initialLocations.getOrDefault(player, player.getWorld().getSpawnLocation()); - // 额外检查:如果初始位置Y坐标过低(比如高空),强制使用世界重生点 - if (initialLoc.getY() > 200) { // 假设Y>200为危险高空 + // 高危位置检测:超过200高度强制使用重生点 + if (initialLoc.getY() > HIGH_ALTITUDE_SAFE) { initialLoc = player.getWorld().getSpawnLocation(); } player.teleport(initialLoc); - - // 7. 恢复游戏模式 + // 7. 恢复生存模式 player.setGameMode(GameMode.SURVIVAL); + // 8. 发送登录成功提示 + player.sendMessage("§a登录成功!欢迎回到星阁钱语"); + player.sendTitle("§b登录成功", "§a祝您游戏愉快", 10, 70, 20); + new BukkitRunnable(){ + @Override + public void run() { + player.getScoreboard().getObjective("playtime").getScore( + player.getName()).setScore(player.getScoreboard().getObjective( + "playtime").getScore(player.getName()).getScore()+1); + // 游玩时间 + // 贡献值 + /* + 贡献值计算方法: + 1. 游玩满 1 min 增加 1 + 2. 游玩满 5 min 增加 1 + 3. 游玩满 10 min 增加 1 + 4. 游玩满 30 min 增加 2 + 5. 游玩满 1h 增加 2 + 6. 游玩大于 1h 增加 1 / 15 min + 以上单位为 : 1d + 7. 一个月以内登录超过 7 天增加 5 + 8. 一个月以内登录超过 14 天增加 10 + 9. 一个月以内登录超过 21 天增加 20 + 10. 一个月以内满勤 + 60 + 以上单位为 : 1mo + 11. 一星期登录超过3天 + 2 + 12. 一星期登录超过5天 + 5 + 13. 一星期满勤 + 11 + 14. 其他活动增加 + 15. 充值{ + 无 *1.0 + vip *1.1 + vip+ *1.2 + MVP *1.4 + MVP+ *1.7 + MVP++ *2.0 + } + 17. 充值点券 100点券(原始值,向下取整,不满100不算) = 1 贡献值(不叠加) + */ + } + }.runTaskTimer(plugin,0,20L); } } \ No newline at end of file diff --git a/src/main/java/org/xgqy/survival/event/RankAdd.java b/src/main/java/org/xgqy/survival/event/RankAdd.java new file mode 100644 index 0000000..f0f9835 --- /dev/null +++ b/src/main/java/org/xgqy/survival/event/RankAdd.java @@ -0,0 +1,267 @@ +package org.xgqy.survival.event; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.xgqy.survival.Survival; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 贡献值管理类:负责贡献值计算、存储、查询、修改 + */ +public class RankAdd { + + // 插件实例 + private final Survival plugin; + // Gson 序列化工具(格式化输出) + private final Gson gson; + // 数据文件(存储所有玩家贡献值) + private final File contributionFile; + + // 内存缓存:玩家UUID -> 贡献值数据 + private Map contributionMap; + + // 贡献类型枚举(可扩展,每种类型对应默认贡献值) + public enum ContributionType { + TASK_MAIN(10), // 主线任务 + TASK_DAILY(5), // 日常任务 + DONATE_ITEM(3), // 捐赠物品 + EVENT_PARTICIPATE(4),// 参与活动 + BUILD_PUBLIC(8), // 建造公共设施 + HELP_PLAYER(2); // 帮助其他玩家 + + private final int defaultValue; // 该类型默认贡献值 + + ContributionType(int defaultValue) { + this.defaultValue = defaultValue; + } + + public int getDefaultValue() { + return defaultValue; + } + } + + /** + * 玩家贡献值实体类(存储单个玩家的贡献数据) + */ + public static class PlayerContribution { + private String playerUuid; // 玩家唯一标识(防改名) + private String playerName; // 玩家当前名称 + private int totalContribution; // 总贡献值 + private long lastUpdateTime; // 最后更新时间(毫秒) + private List records; // 贡献记录(追溯用) + + // Gson 序列化必需的默认构造函数 + public PlayerContribution() {} + + public PlayerContribution(String playerUuid, String playerName) { + this.playerUuid = playerUuid; + this.playerName = playerName; + this.totalContribution = 0; + this.lastUpdateTime = System.currentTimeMillis(); + this.records = new ArrayList<>(); + } + + // Getter 和 Setter + public String getPlayerUuid() { return playerUuid; } + public void setPlayerUuid(String playerUuid) { this.playerUuid = playerUuid; } + public String getPlayerName() { return playerName; } + public void setPlayerName(String playerName) { this.playerName = playerName; } + public int getTotalContribution() { return totalContribution; } + public void setTotalContribution(int totalContribution) { this.totalContribution = totalContribution; } + public long getLastUpdateTime() { return lastUpdateTime; } + public void setLastUpdateTime(long lastUpdateTime) { this.lastUpdateTime = lastUpdateTime; } + public List getRecords() { return records; } + public void setRecords(List records) { this.records = records; } + } + + /** + * 贡献记录实体类(记录每次贡献值变动) + */ + public static class ContributionRecord { + private ContributionType type; // 贡献类型 + private int value; // 变动值(正数=增加,负数=减少) + private long timestamp; // 变动时间(毫秒) + private String reason; // 变动原因(可选,如"完成主线任务:击败末影龙") + + public ContributionRecord(ContributionType type, int value, String reason) { + this.type = type; + this.value = value; + this.timestamp = System.currentTimeMillis(); + this.reason = reason; + } + + // Getter + public ContributionType getType() { return type; } + public int getValue() { return value; } + public long getTimestamp() { return timestamp; } + public String getReason() { return reason; } + } + public List getAllContributions() { + List allData = new ArrayList<>(contributionMap.values()); + allData.sort((d1, d2) -> Integer.compare(d2.getTotalContribution(), d1.getTotalContribution())); + return allData; + } + public RankAdd(Survival plugin) { + this.plugin = plugin; + this.gson = new GsonBuilder().setPrettyPrinting().create(); + this.contributionFile = new File(plugin.getDataFolder(), "contributions.json"); + this.contributionMap = loadData(); + } + public int addContribution(@NotNull Player player, @NotNull ContributionType type, @NotNull String reason) { + return modifyContribution(player, type.getDefaultValue(), type, reason); + } + public int addCustomContribution(@Nullable Player player, int customValue, @NotNull ContributionType type, @NotNull String reason) { + if (customValue <= 0) { + plugin.getLogger().warning("添加贡献值失败:自定义值必须为正数"); + return player != null ? getContribution(player) : 0; + } + + // 处理离线玩家:通过OfflinePlayer的UUID获取数据 + String uuidStr; + String playerName; + if (player != null) { + uuidStr = player.getUniqueId().toString(); + playerName = player.getName(); + } else { + // 若player为null(离线),需从贡献值缓存中查找(仅支持已有记录的离线玩家) + // 注:新增离线玩家需先通过UUID创建数据,此处仅处理已有记录的情况 + return 0; + } + + PlayerContribution data = contributionMap.computeIfAbsent(uuidStr, k -> new PlayerContribution(uuidStr, playerName)); + int newValue = Math.max(0, data.getTotalContribution() + customValue); + data.setTotalContribution(newValue); + data.setLastUpdateTime(System.currentTimeMillis()); + data.setPlayerName(playerName); + data.getRecords().add(new ContributionRecord(type, customValue, reason)); + + saveData(); + plugin.getLogger().info("添加贡献值:" + playerName + " +" + customValue + ",原因:" + reason + ",当前:" + newValue); + return newValue; + } + public int reduceContribution(@NotNull Player player, int reduceValue, @NotNull ContributionType type, @NotNull String reason) { + if (reduceValue <= 0) { + plugin.getLogger().warning("扣除玩家 " + player.getName() + " 贡献值失败:扣除值必须为正数"); + return getContribution(player); + } + return modifyContribution(player, -reduceValue, type, reason); + } + public int getContribution(@NotNull Player player) { + PlayerContribution data = getOrCreatePlayerData(player); + return data.getTotalContribution(); + } + public List getContributionRecords(@NotNull Player player) { + PlayerContribution data = getOrCreatePlayerData(player); + List records = data.getRecords(); + // 倒序排列(最新记录在前) + records.sort((r1, r2) -> Long.compare(r2.getTimestamp(), r1.getTimestamp())); + // 只返回最近10条,避免数据过多 + int end = Math.min(10, records.size()); + return records.subList(0, end); + } + public void resetContribution(@NotNull Player player, @NotNull String reason) { + PlayerContribution data = getOrCreatePlayerData(player); + int oldValue = data.getTotalContribution(); + data.setTotalContribution(0); + data.setLastUpdateTime(System.currentTimeMillis()); + // 添加重置记录(变动值为 -旧值) + data.getRecords().add(new ContributionRecord(ContributionType.TASK_MAIN, -oldValue, "重置贡献值:" + reason)); + saveData(); // 保存到文件 + plugin.getLogger().info("玩家 " + player.getName() + " 的贡献值已重置,原数值:" + oldValue + ",原因:" + reason); + } + public List getTopContributors() { + List allData = new ArrayList<>(contributionMap.values()); + // 按总贡献值降序排序 + allData.sort((d1, d2) -> Integer.compare(d2.getTotalContribution(), d1.getTotalContribution())); + // 返回前10名 + int end = Math.min(10, allData.size()); + return allData.subList(0, end); + } + private int modifyContribution(@NotNull Player player, int changeValue, @NotNull ContributionType type, @NotNull String reason) { + PlayerContribution data = getOrCreatePlayerData(player); + + // 计算新贡献值(最低为0,不允许负贡献) + int newValue = Math.max(0, data.getTotalContribution() + changeValue); + data.setTotalContribution(newValue); + data.setLastUpdateTime(System.currentTimeMillis()); + data.setPlayerName(player.getName()); // 同步最新玩家名称(防止改名后显示异常) + + // 添加贡献记录 + data.getRecords().add(new ContributionRecord(type, changeValue, reason)); + + // 保存到文件 + saveData(); + + // 日志输出 + String action = changeValue > 0 ? "添加" : "扣除"; + plugin.getLogger().info(action + "玩家 " + player.getName() + " 贡献值:" + Math.abs(changeValue) + + ",类型:" + type.name() + ",原因:" + reason + ",当前总贡献:" + newValue); + + return newValue; + } + private PlayerContribution getOrCreatePlayerData(@NotNull Player player) { + UUID uuid = player.getUniqueId(); + String uuidStr = uuid.toString(); + + // 从缓存获取,不存在则创建新数据 + return contributionMap.computeIfAbsent(uuidStr, k -> new PlayerContribution(uuidStr, player.getName())); + } + private Map loadData() { + // 文件不存在则创建空文件 + if (!contributionFile.exists()) { + createFile(contributionFile); + return new HashMap<>(); + } + + try (FileReader reader = new FileReader(contributionFile)) { + Type mapType = new TypeToken>() {}.getType(); + Map loadedMap = gson.fromJson(reader, mapType); + // 防止文件损坏导致的空指针 + return loadedMap != null ? loadedMap : new HashMap<>(); + } catch (IOException e) { + plugin.getLogger().severe("加载贡献值文件失败!" + e.getMessage()); + e.printStackTrace(); + return new HashMap<>(); + } + } + private void saveData() { + try (FileWriter writer = new FileWriter(contributionFile)) { + gson.toJson(contributionMap, writer); + } catch (IOException e) { + plugin.getLogger().severe("保存贡献值文件失败!" + e.getMessage()); + e.printStackTrace(); + } + } + private void createFile(File file) { + try { + file.getParentFile().mkdirs(); // 递归创建父目录(插件data目录) + file.createNewFile(); // 创建文件 + plugin.getLogger().info("创建贡献值数据文件:" + file.getPath()); + } catch (IOException e) { + plugin.getLogger().severe("创建贡献值文件失败!" + file.getPath() + " - " + e.getMessage()); + e.printStackTrace(); + } + } + public int getContributionByName(String playerName) { + for (PlayerContribution data : contributionMap.values()) { + if (data.getPlayerName().equalsIgnoreCase(playerName)) { + return data.getTotalContribution(); + } + } + return -1; + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2021d25..6591e8c 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -86,10 +86,20 @@ commands: notice: description: notice a player usage: / + messageboard: + description: 留言板命令 + usage: / [add|list|support] + aliases: [ mb ] + mb: + description: 留言板命令 + usage: / [add|list|support] permissions: permission.settag: description: Allows setting player tags default: op permission.handle: description: handle players reported + default: op + messageboard.admin: + description: admin default: op \ No newline at end of file