package dev.loki.hook; import dev.loki.mdbd; import org.bukkit.Location; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Display; import org.bukkit.entity.EntityType; import org.bukkit.entity.TextDisplay; import org.bukkit.scheduler.BukkitTask; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * Floating labels above hooks showing their status. * Uses TextDisplay (Paper 1.19.4+) with ArmorStand fallback for older versions. */ public class HookHologram { private final mdbd plugin; private final HookManager manager; private final Map displays = new HashMap<>(); // TextDisplay or ArmorStand private BukkitTask task; private boolean useTextDisplay; public HookHologram(mdbd plugin, HookManager manager) { this.plugin = plugin; this.manager = manager; this.useTextDisplay = isTextDisplayAvailable(); } private static boolean isTextDisplayAvailable() { try { EntityType.valueOf("TEXT_DISPLAY"); return true; } catch (IllegalArgumentException e) { return false; } } public void start() { stop(); spawnAll(); task = plugin.getServer().getScheduler().runTaskTimer(plugin, this::refresh, 20L, 20L); plugin.getLogger().info("[HookHologram] Started with " + displays.size() + " holograms (mode=" + (useTextDisplay ? "TextDisplay" : "ArmorStand") + ")"); } public void stop() { if (task != null) { task.cancel(); task = null; } for (Object display : displays.values()) { if (display instanceof TextDisplay td && td.isValid()) { td.remove(); } else if (display instanceof ArmorStand as && as.isValid()) { as.remove(); } } displays.clear(); } private void spawnAll() { double heightOffset = plugin.getConfigManager().getGame().getHookHologramHeightOffset(); int index = 0; for (Hook hook : manager.getAllHooks()) { Location loc = hook.getLocation(); if (loc.getWorld() == null) { plugin.getLogger().warning("[HookHologram] Hook " + hook.getId() + " has null world, skipping"); continue; } Location holoLoc = loc.clone().add(0.5, 2.5 + heightOffset, 0.5); try { if (useTextDisplay) { TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { d.setBillboard(Display.Billboard.VERTICAL); d.setSeeThrough(true); d.setShadowed(true); d.setDefaultBackground(false); d.setPersistent(false); d.setTextOpacity((byte) -1); d.setViewRange(128.0f); }); displays.put(hook.getId(), display); } else { ArmorStand stand = loc.getWorld().spawn(holoLoc, ArmorStand.class, as -> { as.setInvisible(true); as.setInvulnerable(true); as.setGravity(false); as.setMarker(true); as.setCustomNameVisible(true); as.setPersistent(false); }); displays.put(hook.getId(), stand); } } catch (Exception e) { plugin.getLogger().warning("[HookHologram] Failed to spawn hologram for hook " + hook.getId() + ": " + e.getMessage()); } index++; } } private void refresh() { int total = manager.getAllHooks().size(); int visibleIndex = 0; for (Hook hook : manager.getAllHooks()) { Object display = displays.get(hook.getId()); if (display == null) continue; boolean valid = (display instanceof TextDisplay td && td.isValid()) || (display instanceof ArmorStand as && as.isValid()); if (!valid) continue; String status; if (hook.isDisabled()) { status = "§c✂ Саботирован"; } else if (hook.isOccupied()) { int pct = (int) hook.getStageProgressPercent(); status = "§4Занят [" + hook.getCurrentStage().name() + " " + pct + "%]"; } else { status = "§aСвободен"; } visibleIndex++; String text = "§c⚓ Крюк §f#" + visibleIndex + "/" + total + "\n§7" + status; double heightOffset = plugin.getConfigManager().getGame().getHookHologramHeightOffset(); Location base = hook.getLocation().clone().add(0.5, 2.5 + heightOffset, 0.5); if (display instanceof TextDisplay td) { td.text(net.kyori.adventure.text.Component.text(text)); td.teleport(base); } else if (display instanceof ArmorStand as) { as.setCustomName(text); as.teleport(base); } } } }