新增PVP练习测试场(pvp_test独立世界)与完善/pvp命令补全

- 新增 PVPTestManager:首次加载预生成30x30露天FLAT世界(pvp_test),worldborder边界+玻璃围墙+关闭自然刷怪
- /pvp test 打开GUI面板选择难度(简单/中等/噩梦)与装备(近战/远程/混合)
- 开始:备份背包、发放装备、传送、生成40血僵尸/小白(近战=僵尸/远程=小白/混合=两者)
- 难度固定伤害:简单(僵尸4/小白2)、中等(僵尸4/小白3)、噩梦(僵尸8/小白7)
- 噩梦难度被玩家击中随机走位;测试场敌对生物互不攻击(取消mob→mob目标与伤害、小白箭不伤友军)
- /pvp命令补全补齐 join/leave/exit/test 一级子命令
- /pvp leave|exit 统一经 Main.leavePVP 路由(测试场优先,否则竞技场)
This commit is contained in:
2026-07-09 19:36:29 +08:00
parent 4812c093ea
commit ac92cdb31e
3 changed files with 623 additions and 4 deletions
+42 -3
View File
@@ -173,6 +173,8 @@ public class Main extends JavaPlugin
private PVPManager pvpManager;
//pvp竞技场(独立世界模式)
private PVPArenaManager pvPArenaManager;
//pvp练习测试场(独立世界 pvp_test)
private PVPTestManager pvpTestManager;
@@ -519,6 +521,13 @@ public class Main extends JavaPlugin
pvPArenaManager.ensurePVPWorldExists();
// PVP模板世界改为懒加载:首次 /pvp join 时按需创建(不再启动时同步生成,避免拖慢启动50秒)
// 12.2 ===PVP练习测试场(独立世界 pvp_test,首次加载预生成30x30露天环境)===
pvpTestManager = new PVPTestManager(this);
getServer().getPluginManager()
.registerEvents(pvpTestManager, this);
// 确保测试场世界存在(首次生成/之后复用)
pvpTestManager.ensurePVPTestWorldExists();
// 13 ====cypay债券====
// 债券系统(独立DB)
bondManager = new BondManager(this);
@@ -679,7 +688,9 @@ public class Main extends JavaPlugin
"create", "stats",
"list", "delete",
"tool", "on", "off",
"tempban", "arena"};
"tempban", "arena",
"join", "leave",
"exit", "test"};
for (String s : sub) {
if (s.startsWith(
args[0]
@@ -3201,6 +3212,19 @@ public class Main extends JavaPlugin
public PVPArenaManager getPVPArenaManager() {
return pvPArenaManager;
}
public PVPTestManager getPVPTestManager() {
return pvpTestManager;
}
/** 统一离开 PVP 场景:测试场优先,否则竞技场 */
public void leavePVP(Player p) {
if (pvpTestManager != null && pvpTestManager.isInTest(p)) {
pvpTestManager.leaveTest(p);
} else {
pvPArenaManager.leaveArena(p);
}
}
// 在 Main.java 中(与其他 getter 放一起)
public AreaProtection getAreaProtection() {
return areaProtection;
@@ -3530,6 +3554,15 @@ public class Main extends JavaPlugin
return;
}
// ===== PVP练习测试场GUI点击处理 =====
if (title.equals("§6§lPVP测试场")) {
if (pvpTestManager != null) {
e.setCancelled(true);
pvpTestManager.handleClick(p, e.getRawSlot());
}
return;
}
// 传送面板不受登录冻结限制(基岩版玩家免登录也要能用)
if (!title.equals("§6§l传送系统")) {
if (isFrozen(p)) {
@@ -5098,12 +5131,18 @@ public class Main extends JavaPlugin
return true;
}
Player p = (Player) sender;
// PVP练习测试场:/pvp test 打开设置面板
if (args.length > 0 && args[0].equalsIgnoreCase("test")) {
pvpTestManager.openGUI(p);
return true;
}
// PVP竞技场命令(由pvpManager统一处理)
if (args.length > 0 && args[0].equalsIgnoreCase("arena")) {
return pvpManager.onCommand(p, args);
}
return pvpManager.onCommand(p, args);
}
+1 -1
View File
@@ -1120,7 +1120,7 @@ public class PVPManager implements Listener {
return true;
case "leave":
case "exit":
plugin.getPVPArenaManager().leaveArena(p);
plugin.leavePVP(p);
return true;
default:
showStats(p, p.getName());
@@ -0,0 +1,580 @@
package Sdf1_login;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.entity.*;
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.EntityTargetLivingEntityEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.projectiles.ProjectileSource;
import org.bukkit.util.Vector;
import java.io.File;
import java.util.*;
/**
* PVP 练习测试场管理器
*
* ★ 设计(2026-07-09 按用户需求):
* - 插件首次加载时在独立世界 pvp_test 预生成 30×30 露天环境(FLAT 世界 + worldborder 边界 + 玻璃围墙),
* 之后复用该世界(不随玩家离开而删除)。
* - 玩家输入 /pvp test 打开 GUI 面板:选择【难度】(简单/中等/噩梦) 与【装备】(近战/远程/混合) 后点击确认开始。
* - 开始后:备份并清空原背包 → 发放对应练习装备 → 传送至测试场 → 按装备类型生成练习生物
* (近战=僵尸 / 远程=小白 / 混合=僵尸+小白),所有练习生物血量统一 40。
* - 难度固定伤害(每次攻击):简单(僵尸4/小白2)、中等(僵尸4/小白3)、噩梦(僵尸8/小白7)。
* - 噩梦难度:被玩家击中时随机走位(闪避)。
* - 测试场景下所有敌对生物【无视队友攻击】:取消 mob→mob 的目标与伤害、小白箭不伤友军。
* - 离开(/pvp leave) 或死亡/退出:清除练习生物、还原背包、传回主世界。
*/
public class PVPTestManager implements Listener {
private final Main plugin;
public static final String PVP_TEST_WORLD_NAME = "pvp_test";
private static final String TEST_GUI_TITLE = "§6§lPVP测试场";
/** 在场练习的玩家 */
private final Set<UUID> inTest = new HashSet<>();
/** 每个玩家选择的难度/装备(GUI 暂存) */
private final Map<UUID, TestDifficulty> chosenDiff = new HashMap<>();
private final Map<UUID, TestEquip> chosenEquip = new HashMap<>();
/** 每个练习生物对应的难度(用于固定伤害 + 噩梦走位判定) */
private final Map<UUID, TestDifficulty> mobDifficulty = new HashMap<>();
/** 每个玩家生成的练习生物 UUID 列表(离开时按玩家清理) */
private final Map<UUID, List<UUID>> playerMobs = new HashMap<>();
/** 玩家原背包备份(仅内存,练习场为临时场景) */
private final Map<UUID, InventoryBackup> inventoryBackups = new HashMap<>();
private final Random rand = new Random();
// ==================== 枚举定义 ====================
enum TestDifficulty {
EASY("§a简单", 4.0, 2.0),
MEDIUM("§e中等", 4.0, 3.0),
NIGHTMARE("§c噩梦", 8.0, 7.0);
final String display;
final double zombieDamage;
final double skeletonDamage;
TestDifficulty(String display, double zombieDamage, double skeletonDamage) {
this.display = display;
this.zombieDamage = zombieDamage;
this.skeletonDamage = skeletonDamage;
}
}
enum TestEquip {
MELEE("§c近战"),
RANGED("§b远程"),
MIXED("§6混合");
final String display;
TestEquip(String display) {
this.display = display;
}
}
/** 背包备份结构 */
private static class InventoryBackup {
final ItemStack[] contents;
final ItemStack[] armor;
final ItemStack offHand;
final int level;
final float exp;
InventoryBackup(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, int level, float exp) {
this.contents = contents;
this.armor = armor;
this.offHand = offHand;
this.level = level;
this.exp = exp;
}
}
public PVPTestManager(Main plugin) {
this.plugin = plugin;
}
// ==================== 世界初始化 ====================
/**
* 确保测试场世界存在(首次加载时生成;之后若磁盘有残留则直接加载复用)。
* 在主线程同步执行(Bukkit.createWorld 触发 WorldInitEvent 必须主线程)。
* FLAT 世界生成极快,不会卡服。
*/
public void ensurePVPTestWorldExists() {
File folder = new File(Bukkit.getWorldContainer(), PVP_TEST_WORLD_NAME);
boolean fresh = !folder.exists();
World w = Bukkit.getWorld(PVP_TEST_WORLD_NAME);
if (w == null) {
plugin.getLogger().info("[PVP测试] 创建/加载测试场世界 " + PVP_TEST_WORLD_NAME
+ (fresh ? " (首次生成)" : " (从磁盘加载)"));
WorldCreator c = new WorldCreator(PVP_TEST_WORLD_NAME);
c.environment(World.Environment.NORMAL);
c.type(WorldType.FLAT);
c.generateStructures(false);
c.seed(20260709L); // 固定种子,保证环境一致
w = Bukkit.createWorld(c);
}
if (w == null) {
plugin.getLogger().severe("[PVP测试] 无法创建测试场世界!");
return;
}
// 世界规则:常昼 / 无天气 / 关闭自然刷怪(只有我们生成的练习生物)/ 死亡不掉装备 / PVP 开启
w.setPVP(true);
w.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
w.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
w.setGameRule(GameRule.DO_MOB_SPAWNING, false);
w.setGameRule(GameRule.KEEP_INVENTORY, true);
w.setTime(6000);
w.setKeepSpawnInMemory(false);
// 30×30 世界边界(中心 0,0,直径 30)
WorldBorder wb = w.getWorldBorder();
wb.setCenter(0, 0);
wb.setSize(30.0);
wb.setDamageAmount(0.0); // 仅阻挡不造成伤害
wb.setWarningDistance(0);
// 首次生成时搭建边界玻璃墙
if (fresh) {
buildBoundary(w);
}
// 预加载出生点区块
w.loadChunk(w.getSpawnLocation().getChunk());
plugin.getLogger().info("[PVP测试] 测试场世界就绪 (FLAT露天, 30x30边界, 自然刷怪已关闭)");
}
/** 在边界(±15)搭建 4 格高玻璃墙,作为露天练习场的可见边界 */
private void buildBoundary(World w) {
int r = 15;
int baseY = w.getHighestBlockYAt(0, 0) + 1;
for (int i = -r; i <= r; i++) {
for (int h = 0; h < 4; h++) {
setBlock(w, i, baseY + h, -r, Material.GLASS);
setBlock(w, i, baseY + h, r, Material.GLASS);
setBlock(w, -r, baseY + h, i, Material.GLASS);
setBlock(w, r, baseY + h, i, Material.GLASS);
}
}
plugin.getLogger().info("[PVP测试] 已搭建 30x30 边界玻璃墙");
}
private void setBlock(World w, int x, int y, int z, Material m) {
Block b = w.getBlockAt(x, y, z);
if (b.getType().isAir()) b.setType(m);
}
/** 测试场安全出生点(FLAT 世界地表上方) */
private Location getSafeSpawn(World w) {
int y = w.getHighestBlockYAt(0, 0) + 1;
return new Location(w, 0.5, y, 0.5);
}
// ==================== GUI ====================
public void openGUI(Player p) {
Inventory gui = Bukkit.createInventory(null, 54, TEST_GUI_TITLE);
TestDifficulty diff = chosenDiff.getOrDefault(p.getUniqueId(), TestDifficulty.EASY);
TestEquip equip = chosenEquip.getOrDefault(p.getUniqueId(), TestEquip.MELEE);
// 顶部信息
ItemStack info = new ItemStack(Material.PAPER);
ItemMeta im = info.getItemMeta();
if (im != null) {
im.setDisplayName("§6§lPVP测试场设置");
im.setLore(Arrays.asList(
"§7选择难度与装备,点击确认开始练习",
"",
"§e当前难度: " + diff.display,
"§e当前装备: " + equip.display));
info.setItemMeta(im);
}
gui.setItem(4, info);
setDiffButton(gui, 19, TestDifficulty.EASY, diff);
setDiffButton(gui, 21, TestDifficulty.MEDIUM, diff);
setDiffButton(gui, 23, TestDifficulty.NIGHTMARE, diff);
setEquipButton(gui, 28, TestEquip.MELEE, equip);
setEquipButton(gui, 30, TestEquip.RANGED, equip);
setEquipButton(gui, 32, TestEquip.MIXED, equip);
// 确认开始按钮
ItemStack start = new ItemStack(Material.LIME_STAINED_GLASS_PANE);
ItemMeta sm = start.getItemMeta();
if (sm != null) {
sm.setDisplayName("§a§l确认开始");
sm.setLore(Arrays.asList(
"§7难度: " + diff.display,
"§7装备: " + equip.display,
"",
"§e点击进入测试场"));
start.setItemMeta(sm);
}
gui.setItem(49, start);
// 装饰填充
for (int i = 0; i < 54; i++) {
if (gui.getItem(i) == null) {
gui.setItem(i, new ItemStack(Material.GRAY_STAINED_GLASS_PANE));
}
}
p.openInventory(gui);
}
private void setDiffButton(Inventory gui, int slot, TestDifficulty d, TestDifficulty current) {
boolean selected = (d == current);
ItemStack btn = new ItemStack(selected ? Material.LIME_STAINED_GLASS_PANE : Material.RED_STAINED_GLASS_PANE);
ItemMeta m = btn.getItemMeta();
if (m != null) {
m.setDisplayName(d.display);
List<String> lore = new ArrayList<>();
lore.add("§7僵尸伤害: " + (int) d.zombieDamage);
lore.add("§7小白伤害: " + (int) d.skeletonDamage);
if (d == TestDifficulty.NIGHTMARE) lore.add("§c被攻击时随机走位");
lore.add(selected ? "§a✓ 已选中" : "§e点击选择");
m.setLore(lore);
btn.setItemMeta(m);
}
gui.setItem(slot, btn);
}
private void setEquipButton(Inventory gui, int slot, TestEquip e, TestEquip current) {
boolean selected = (e == current);
Material icon = e == TestEquip.MELEE ? Material.IRON_SWORD
: e == TestEquip.RANGED ? Material.BOW : Material.NETHER_STAR;
ItemStack btn = new ItemStack(icon);
ItemMeta m = btn.getItemMeta();
if (m != null) {
m.setDisplayName(e.display);
List<String> lore = new ArrayList<>();
lore.add(e == TestEquip.MELEE ? "§7练习近战(僵尸)" :
e == TestEquip.RANGED ? "§7练习远程(小白)" : "§7练习近战+远程");
lore.add(selected ? "§a✓ 已选中" : "§e点击选择");
m.setLore(lore);
btn.setItemMeta(m);
}
gui.setItem(slot, btn);
}
public void handleClick(Player p, int slot) {
if (!p.getOpenInventory().getTitle().equals(TEST_GUI_TITLE)) return;
if (slot == 19) { chosenDiff.put(p.getUniqueId(), TestDifficulty.EASY); openGUI(p); return; }
if (slot == 21) { chosenDiff.put(p.getUniqueId(), TestDifficulty.MEDIUM); openGUI(p); return; }
if (slot == 23) { chosenDiff.put(p.getUniqueId(), TestDifficulty.NIGHTMARE); openGUI(p); return; }
if (slot == 28) { chosenEquip.put(p.getUniqueId(), TestEquip.MELEE); openGUI(p); return; }
if (slot == 30) { chosenEquip.put(p.getUniqueId(), TestEquip.RANGED); openGUI(p); return; }
if (slot == 32) { chosenEquip.put(p.getUniqueId(), TestEquip.MIXED); openGUI(p); return; }
if (slot == 49) {
p.closeInventory();
startTest(p,
chosenDiff.getOrDefault(p.getUniqueId(), TestDifficulty.EASY),
chosenEquip.getOrDefault(p.getUniqueId(), TestEquip.MELEE));
return;
}
}
// ==================== 开始 / 离开 ====================
public void startTest(Player p, TestDifficulty diff, TestEquip equip) {
World w = Bukkit.getWorld(PVP_TEST_WORLD_NAME);
if (w == null) {
p.sendMessage("§c§l[PVP测试] 测试场世界未加载,请联系管理员");
return;
}
// 清掉该玩家之前的练习生物(支持重新配置)
removePlayerMobs(p);
// 传送至测试场
p.teleport(getSafeSpawn(w));
// 备份并清空背包 → 发放练习装备
backupInventory(p);
clearInventory(p);
giveEquip(p, equip);
inTest.add(p.getUniqueId());
// 生成练习生物
spawnMobs(p, w, diff, equip);
p.sendMessage("§a§l[PVP测试] 已进入测试场! 难度: " + diff.display + " 装备: " + equip.display);
p.sendMessage("§7练习生物血量统一为40。敌对生物互不攻击(友伤免疫)。输入 /pvp leave 离开。");
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
plugin.getLogger().info("[PVP测试] 玩家 " + p.getName()
+ " 进入测试场 (难度=" + diff.name() + ", 装备=" + equip.name() + ")");
}
private void giveEquip(Player p, TestEquip equip) {
ItemStack[] ironArmor = new ItemStack[]{
new ItemStack(Material.IRON_BOOTS),
new ItemStack(Material.IRON_LEGGINGS),
new ItemStack(Material.IRON_CHESTPLATE),
new ItemStack(Material.IRON_HELMET)};
ItemStack[] leatherArmor = new ItemStack[]{
new ItemStack(Material.LEATHER_BOOTS),
new ItemStack(Material.LEATHER_LEGGINGS),
new ItemStack(Material.LEATHER_CHESTPLATE),
new ItemStack(Material.LEATHER_HELMET)};
switch (equip) {
case MELEE:
p.getInventory().addItem(new ItemStack(Material.IRON_SWORD));
p.getInventory().setArmorContents(ironArmor);
break;
case RANGED:
p.getInventory().addItem(new ItemStack(Material.BOW));
p.getInventory().addItem(new ItemStack(Material.ARROW, 64));
p.getInventory().setArmorContents(leatherArmor);
break;
case MIXED:
p.getInventory().addItem(new ItemStack(Material.IRON_SWORD));
p.getInventory().addItem(new ItemStack(Material.BOW));
p.getInventory().addItem(new ItemStack(Material.ARROW, 64));
p.getInventory().setArmorContents(ironArmor);
break;
}
}
private void spawnMobs(Player p, World w, TestDifficulty diff, TestEquip equip) {
if (equip == TestEquip.MIXED) {
// 2 僵尸 + 2 小白
spawnRing(p, w, diff, EntityType.ZOMBIE, 2);
spawnRing(p, w, diff, EntityType.SKELETON, 2);
} else if (equip == TestEquip.RANGED) {
spawnRing(p, w, diff, EntityType.SKELETON, 4);
} else {
spawnRing(p, w, diff, EntityType.ZOMBIE, 4);
}
}
private void spawnRing(Player p, World w, TestDifficulty diff, EntityType type, int count) {
Location base = p.getLocation();
for (int i = 0; i < count; i++) {
double ang = (Math.PI * 2.0 * i) / count;
double rad = 5.0;
double x = base.getX() + Math.cos(ang) * rad;
double z = base.getZ() + Math.sin(ang) * rad;
// 钳制在边界内 (±14)
x = Math.max(-14, Math.min(14, x));
z = Math.max(-14, Math.min(14, z));
int y = w.getHighestBlockYAt((int) Math.floor(x), (int) Math.floor(z)) + 1;
Location loc = new Location(w, x, y, z);
Entity ent = w.spawnEntity(loc, type);
if (ent instanceof LivingEntity mob) {
mob.setMaxHealth(40.0);
mob.setHealth(40.0);
mob.setRemoveWhenFarAway(false);
if (mob instanceof Mob m) {
m.setPersistent(true);
m.setTarget(p);
}
mobDifficulty.put(mob.getUniqueId(), diff);
playerMobs.computeIfAbsent(p.getUniqueId(), k -> new ArrayList<>()).add(mob.getUniqueId());
}
}
}
public void leaveTest(Player p) {
if (!inTest.contains(p.getUniqueId())) {
p.sendMessage("§e你不在PVP测试场");
return;
}
removePlayerMobs(p);
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
restoreInventory(p);
World main = Bukkit.getWorlds().get(0);
p.teleport(main.getSpawnLocation());
p.sendMessage("§a§l[PVP测试] 已离开测试场,背包已还原");
}
private void removePlayerMobs(Player p) {
UUID pid = p.getUniqueId();
List<UUID> list = playerMobs.remove(pid);
if (list != null) {
for (UUID uid : list) {
Entity e = Bukkit.getEntity(uid);
if (e != null) e.remove();
mobDifficulty.remove(uid);
}
}
}
// ==================== 背包备份/还原 ====================
private void backupInventory(Player p) {
inventoryBackups.put(p.getUniqueId(), new InventoryBackup(
p.getInventory().getContents().clone(),
p.getInventory().getArmorContents().clone(),
p.getInventory().getItemInOffHand() != null ? p.getInventory().getItemInOffHand().clone() : null,
p.getLevel(),
p.getExp()));
plugin.getLogger().info("[PVP测试] 已备份玩家 " + p.getName() + " 的背包");
}
private void restoreInventory(Player p) {
InventoryBackup b = inventoryBackups.remove(p.getUniqueId());
if (b == null) {
plugin.getLogger().warning("[PVP测试] 未找到玩家 " + p.getName() + " 的背包备份");
return;
}
p.getInventory().setContents(b.contents);
p.getInventory().setArmorContents(b.armor);
p.getInventory().setItemInOffHand(b.offHand);
p.setLevel(b.level);
p.setExp(b.exp);
plugin.getLogger().info("[PVP测试] 已还原玩家 " + p.getName() + " 的背包");
}
private void clearInventory(Player p) {
p.getInventory().clear();
p.getInventory().setArmorContents(new ItemStack[4]);
p.getInventory().setItemInOffHand(null);
}
// ==================== 查询 ====================
public boolean isInTest(Player p) {
return inTest.contains(p.getUniqueId());
}
public boolean isInTestWorld(Player p) {
return isTestWorld(p.getWorld());
}
private boolean isTestWorld(World w) {
return w != null && w.getName().equals(PVP_TEST_WORLD_NAME);
}
private boolean isTestMob(Entity e) {
return e instanceof LivingEntity && mobDifficulty.containsKey(e.getUniqueId());
}
private LivingEntity getAttackerMob(Entity damager) {
if (damager instanceof LivingEntity le && isTestMob(le)) return le;
if (damager instanceof Arrow arrow) {
ProjectileSource src = arrow.getShooter();
if (src instanceof LivingEntity le && isTestMob(le)) return le;
}
return null;
}
// ==================== 事件监听 ====================
/** 固定伤害 + 友伤免疫 + 噩梦随机走位 */
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false)
public void onEntityDamage(EntityDamageByEntityEvent e) {
if (!isTestWorld(e.getEntity().getWorld())) return;
Entity victim = e.getEntity();
Entity damager = e.getDamager();
// 1) 友伤免疫:练习生物之间互不承担伤害(含小白的箭)
boolean victimIsMob = isTestMob(victim);
boolean damagerIsMob = isTestMob(damager) || isTestArrowFromMob(damager);
if (victimIsMob && damagerIsMob) {
e.setCancelled(true);
return;
}
// 2) 玩家被练习生物攻击 → 按难度固定伤害
LivingEntity attacker = getAttackerMob(damager);
if (attacker != null && victim instanceof Player) {
TestDifficulty diff = mobDifficulty.get(attacker.getUniqueId());
if (diff != null) {
double dmg;
if (attacker instanceof Zombie) dmg = diff.zombieDamage;
else if (attacker instanceof Skeleton) dmg = diff.skeletonDamage;
else dmg = e.getDamage();
e.setDamage(dmg);
}
}
// 3) 噩梦难度:被玩家击中时随机走位(闪避)
if (victimIsMob && damager instanceof Player) {
TestDifficulty diff = mobDifficulty.get(victim.getUniqueId());
if (diff == TestDifficulty.NIGHTMARE && victim instanceof LivingEntity mob) {
double ang = rand.nextDouble() * Math.PI * 2.0;
double sp = 0.45;
mob.setVelocity(new Vector(Math.cos(ang) * sp, 0.25, Math.sin(ang) * sp));
}
}
}
private boolean isTestArrowFromMob(Entity damager) {
if (damager instanceof Arrow arrow) {
ProjectileSource src = arrow.getShooter();
return src instanceof LivingEntity le && isTestMob(le);
}
return false;
}
/** 练习生物只锁定玩家,互不锁定(跳过原版 mob→mob 仇恨规则) */
@EventHandler
public void onTarget(EntityTargetLivingEntityEvent e) {
if (!isTestWorld(e.getEntity().getWorld())) return;
if (isTestMob(e.getEntity()) && e.getTarget() != null && isTestMob(e.getTarget())) {
e.setCancelled(true);
}
}
/** 死亡:保留背包(KEEP_INVENTORY),送回主世界并还原 */
@EventHandler
public void onRespawn(PlayerRespawnEvent e) {
Player p = e.getPlayer();
if (!inTest.contains(p.getUniqueId())) return;
removePlayerMobs(p);
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
World main = Bukkit.getWorlds().get(0);
e.setRespawnLocation(main.getSpawnLocation());
// 回到主世界后再还原背包,避免死亡瞬间覆盖
final Player fp = p;
Bukkit.getScheduler().runTaskLater(plugin, () -> restoreInventory(fp), 1L);
}
/** 退出:清理练习生物并还原背包 */
@EventHandler
public void onQuit(PlayerQuitEvent e) {
Player p = e.getPlayer();
if (!inTest.contains(p.getUniqueId())) return;
removePlayerMobs(p);
restoreInventory(p);
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
}
/** 通过其他方式离开测试世界(如指令传送)→ 视为离开 */
@EventHandler
public void onChangedWorld(PlayerChangedWorldEvent e) {
Player p = e.getPlayer();
if (!inTest.contains(p.getUniqueId())) return;
if (isTestWorld(p.getWorld())) return; // 仍在测试场
removePlayerMobs(p);
restoreInventory(p);
inTest.remove(p.getUniqueId());
chosenDiff.remove(p.getUniqueId());
chosenEquip.remove(p.getUniqueId());
}
}