PVP测试场:改为全清后才间隔10秒刷,全清给可点击回床链接

- 撤掉每10秒无脑刷的全局定时器,改为事件驱动:监听 EntityDeathEvent 反查练习生物所属玩家
- 玩家清空当前波次(无存活练习生物)后:发送提示 + 可点击超链接[/pvp back 返回床],并排程10秒后刷下一波
- 新增 mobOwner 反查表 + pendingRespawn 防重复排程;退场(离开/死亡/退出/跨世界)均清理 pendingRespawn
- /pvp back 子命令:在测试场先清理还原背包,再传送至床(出生点),无床则回主世界出生点
- checkAllLeft 改为无人时还原测试场难度(和平模式临时提困难语义保持)
This commit is contained in:
2026-07-09 20:30:38 +08:00
parent 5fa12cddbf
commit 4c100a0c9f
2 changed files with 94 additions and 39 deletions
+6
View File
@@ -5138,6 +5138,12 @@ public class Main extends JavaPlugin
return true;
}
// PVP练习测试场:/pvp back 返回出生点(床),由测试场管理器处理
if (args.length > 0 && args[0].equalsIgnoreCase("back")) {
pvpTestManager.handleBack(p);
return true;
}
// PVP竞技场命令(由pvpManager统一处理)
if (args.length > 0 && args[0].equalsIgnoreCase("arena")) {
return pvpManager.onCommand(p, args);
+88 -39
View File
@@ -7,6 +7,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerQuitEvent;
@@ -18,8 +19,11 @@ import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.projectiles.ProjectileSource;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import java.io.File;
import java.util.*;
@@ -58,12 +62,12 @@ public class PVPTestManager implements Listener {
private final Random rand = new Random();
/** 刷怪波次间隔(刻):10秒 */
/** 刷怪波次间隔(刻):10秒(玩家全清当前波次后才间隔刷下一波) */
private static final long WAVE_INTERVAL_TICKS = 200L;
/** 单个玩家在场练习生物存活上限(安全阀,防止无限堆积卡服) */
private static final int MAX_ALIVE_PER_PLAYER = 30;
/** 死循环刷怪任务(有人在场时运行,全部退场即取消) */
private BukkitTask spawnTask = null;
/** 已安排"全清后10秒补一波"的玩家(防止重复排程) */
private final Set<UUID> pendingRespawn = new HashSet<>();
/** 练习生物 → 所属玩家(用于死亡事件反查触发下一波) */
private final Map<UUID, UUID> mobOwner = new HashMap<>();
// ==================== 枚举定义 ====================
@@ -330,13 +334,12 @@ public class PVPTestManager implements Listener {
inTest.add(p.getUniqueId());
// 立即来一波,之后每10秒一波,死循环直到所有人退场
// 立即来一波(之后仅在玩家清空当前波次后,间隔10秒再补下一波)
spawnWaveFor(p);
startSpawnLoop();
p.sendMessage("§a§l[PVP测试] 已进入测试场! 难度: " + diff.display + " 装备: " + equip.display);
p.sendMessage("§7练习生物血量统一为40。敌对生物互不攻击(友伤免疫)。");
p.sendMessage("§7每10秒刷新一波,背包已附金苹果等回血食物。输入 /pvp leave 离开。");
p.sendMessage("§7清空当前波次后,每10秒刷新下一波;背包已附金苹果等回血食物。输入 /pvp leave 离开。");
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
plugin.getLogger().info("[PVP测试] 玩家 " + p.getName()
+ " 进入测试场 (难度=" + diff.name() + ", 装备=" + equip.name()
@@ -384,7 +387,6 @@ public class PVPTestManager implements Listener {
*/
private void spawnWaveFor(Player p) {
if (!inTest.contains(p.getUniqueId())) return;
if (aliveMobCount(p) >= MAX_ALIVE_PER_PLAYER) return; // 安全阀:超上限则跳过本波
TestDifficulty diff = chosenDiff.get(p.getUniqueId());
if (diff == null) return;
TestEquip equip = chosenEquip.get(p.getUniqueId());
@@ -400,37 +402,71 @@ public class PVPTestManager implements Listener {
}
}
/** 死循环刷怪:每10秒为一波,遍历所有在场玩家各补一波,全部退场则取消任务 */
private void spawnWave() {
if (inTest.isEmpty()) {
stopSpawnLoop();
return;
}
for (UUID uid : new HashSet<>(inTest)) {
Player p = Bukkit.getPlayer(uid);
if (p != null && p.isOnline()) spawnWaveFor(p);
/**
* 玩家全清当前波次 → 发送提示 + 可点击超链接(返回床),并安排 10秒后刷下一波。
* 仅在玩家在场、尚未安排、且当前确实无存活练习生物时触发;避免重复排程。
*/
private void checkCleared(Player p) {
if (!inTest.contains(p.getUniqueId())) return;
if (pendingRespawn.contains(p.getUniqueId())) return;
if (aliveMobCount(p) > 0) return;
pendingRespawn.add(p.getUniqueId());
sendClearedMessage(p);
// 间隔10秒后刷新下一波(若届时仍在场且无存活生物)
Bukkit.getScheduler().runTaskLater(plugin, () -> {
pendingRespawn.remove(p.getUniqueId());
if (inTest.contains(p.getUniqueId()) && aliveMobCount(p) == 0) {
spawnWaveFor(p);
}
}, WAVE_INTERVAL_TICKS);
}
/** 全清提示 + 可点击超链接:点击直接返回出生点(床) */
private void sendClearedMessage(Player p) {
Component link = Component.text("[点击返回出生点(床)]")
.color(NamedTextColor.AQUA)
.clickEvent(ClickEvent.runCommand("/pvp back"))
.hoverEvent(HoverEvent.showText(Component.text("§7点击直接传送回你的床/出生点")));
p.sendMessage(Component.text("§a§l[PVP测试] §f已清空当前波次!§e10秒后刷新下一波。 ")
.append(link));
}
/** 练习生物死亡 → 反查所属玩家,检测是否全清以触发下一波 */
@EventHandler
public void onMobDeath(EntityDeathEvent e) {
LivingEntity ent = e.getEntity();
UUID owner = mobOwner.get(ent.getUniqueId());
if (owner == null) return;
Player p = Bukkit.getPlayer(owner);
if (p != null && p.isOnline()) {
checkCleared(p);
}
}
/** 启动刷怪循环(若未运行) */
private void startSpawnLoop() {
if (spawnTask != null) return;
spawnTask = Bukkit.getScheduler().runTaskTimer(
plugin, this::spawnWave, WAVE_INTERVAL_TICKS, WAVE_INTERVAL_TICKS);
plugin.getLogger().info("[PVP测试] 已启动死循环刷怪(每" + (WAVE_INTERVAL_TICKS / 20) + "秒一波)");
}
/** 停止刷怪循环并还原测试场难度(无人时调用) */
private void stopSpawnLoop() {
if (spawnTask != null) {
spawnTask.cancel();
spawnTask = null;
plugin.getLogger().info("[PVP测试] 刷怪循环已停止(无人)");
/**
* 返回出生点(床):若在测试场则先清理并还原背包,再传送至床(无床则主世界出生点)。
* 供可点击超链接 /pvp back 调用。
*/
public void handleBack(Player p) {
if (inTest.contains(p.getUniqueId())) {
removePlayerMobs(p);
restoreInventory(p);
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
pendingRespawn.remove(p.getUniqueId());
checkAllLeft();
}
World w = Bukkit.getWorld(PVP_TEST_WORLD_NAME);
if (w != null) {
// 还原为当前主世界难度(主世界和平则回到和平,符合"仅临时调整"语义)
w.setDifficulty(Bukkit.getWorlds().get(0).getDifficulty());
Location bed = p.getBedSpawnLocation();
World main = Bukkit.getWorlds().get(0);
Location dest = (bed != null) ? bed : main.getSpawnLocation();
p.teleport(dest);
if (bed != null) {
p.sendMessage("§a§l[PVP测试] 已传送回你的床(出生点)");
} else {
p.sendMessage("§e你尚未设置床,已传送回主世界出生点");
}
}
@@ -447,9 +483,16 @@ public class PVPTestManager implements Listener {
return n;
}
/** 全部退场后停止刷怪循环(恢复难度由 stopSpawnLoop 负责) */
/** 全部退场后还原测试场难度(和平模式下临时提困难,此时回到主世界难度) */
private void checkAllLeft() {
if (inTest.isEmpty()) stopSpawnLoop();
if (inTest.isEmpty()) {
World w = Bukkit.getWorld(PVP_TEST_WORLD_NAME);
if (w != null) {
Difficulty d = Bukkit.getWorlds().get(0).getDifficulty();
w.setDifficulty(d);
plugin.getLogger().info("[PVP测试] 测试场已无人,难度还原为 " + d.name());
}
}
}
private void spawnRing(Player p, World w, TestDifficulty diff, EntityType type, int count) {
@@ -493,6 +536,7 @@ public class PVPTestManager implements Listener {
eq.setHelmetDropChance(0f); // 不会被打掉/掉落
}
mobDifficulty.put(mob.getUniqueId(), diff);
mobOwner.put(mob.getUniqueId(), p.getUniqueId());
playerMobs.computeIfAbsent(p.getUniqueId(), k -> new ArrayList<>()).add(mob.getUniqueId());
}
}
@@ -507,6 +551,7 @@ public class PVPTestManager implements Listener {
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
pendingRespawn.remove(p.getUniqueId());
restoreInventory(p);
World main = Bukkit.getWorlds().get(0);
p.teleport(main.getSpawnLocation());
@@ -522,6 +567,7 @@ public class PVPTestManager implements Listener {
Entity e = Bukkit.getEntity(uid);
if (e != null) e.remove();
mobDifficulty.remove(uid);
mobOwner.remove(uid);
}
}
}
@@ -653,6 +699,7 @@ public class PVPTestManager implements Listener {
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
pendingRespawn.remove(p.getUniqueId());
World main = Bukkit.getWorlds().get(0);
e.setRespawnLocation(main.getSpawnLocation());
// 回到主世界后再还原背包,避免死亡瞬间覆盖
@@ -671,6 +718,7 @@ public class PVPTestManager implements Listener {
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
pendingRespawn.remove(p.getUniqueId());
checkAllLeft();
}
@@ -685,6 +733,7 @@ public class PVPTestManager implements Listener {
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
pendingRespawn.remove(p.getUniqueId());
checkAllLeft();
}
}