diff --git a/build.gradle b/build.gradle
index c5df20b..2d19335 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ plugins {
}
group = 'gg.nextforge'
-version = '1.0'
+version = '2.0-SNAPSHOT'
repositories {
mavenCentral()
@@ -21,6 +21,8 @@ dependencies {
implementation "org.mozilla:rhino:1.7.14"
implementation "com.google.code.gson:gson:2.10.1"
implementation "org.bstats:bstats-bukkit:3.0.2"
+ testImplementation "org.junit.jupiter:junit-jupiter:5.9.3"
+ testImplementation "org.mockito:mockito-core:5.5.0"
}
subprojects {
@@ -67,11 +69,21 @@ java {
withSourcesJar()
}
+javadoc {
+ options.encoding = 'UTF-8'
+ options.addStringOption('Xdoclint:none', '-quiet')
+ failOnError = false
+}
+
tasks {
compileJava {
options.encoding = "UTF-8"
}
+ test {
+ useJUnitPlatform()
+ }
+
// Haupt-Versionserweiterung für plugin.yml
processResources {
filesMatching("plugin.yml") {
@@ -93,13 +105,21 @@ shadowJar {
publishing {
repositories {
- mavenLocal()
+ maven {
+ name = 'NextForge'
+ url = uri("http://87.106.178.7:4231/releases")
+ allowInsecureProtocol = true
+ credentials {
+ username = "push_access"
+ password = project.findProperty("core_token") ?: System.getenv("CORE_TOKEN") ?: ""
+ }
+ }
}
publications {
create("mavenJava", MavenPublication) {
from components.java
groupId = project.group
- artifactId = 'nextforge'
+ artifactId = 'nextcore'
version = project.version
pom {
diff --git a/src/main/java/gg/nextforge/NextCorePlugin.java b/src/main/java/gg/nextforge/NextCorePlugin.java
index 7821a84..bf828fc 100644
--- a/src/main/java/gg/nextforge/NextCorePlugin.java
+++ b/src/main/java/gg/nextforge/NextCorePlugin.java
@@ -1,16 +1,18 @@
package gg.nextforge;
+import gg.nextforge.command.builtin.HologramCommand;
import gg.nextforge.command.builtin.NPCCommand;
import gg.nextforge.command.builtin.NextCoreCommand;
import gg.nextforge.config.ConfigFile;
-import gg.nextforge.config.ConfigManager;
import gg.nextforge.console.ConsoleHeader;
+import gg.nextforge.event.EventBus;
+import gg.nextforge.npc.NPCListener;
import gg.nextforge.plugin.NextForgePlugin;
import gg.nextforge.scheduler.CoreScheduler;
import gg.nextforge.scheduler.ScheduledTask;
import gg.nextforge.updater.CoreAutoUpdater;
import lombok.Getter;
-import org.checkerframework.checker.units.qual.N;
+import org.bukkit.Bukkit;
import java.io.IOException;
import java.util.UUID;
@@ -48,6 +50,11 @@ public void enable(boolean isReload) {
new NextCoreCommand(this);
new NPCCommand(this, getNpcManager());
+ new HologramCommand(this, getHologramManager());
+
+ if (getConfigFile().getBoolean("commands.npc.enabled", true)) {
+ Bukkit.getPluginManager().registerEvents(new NPCListener(getNpcManager()), this);
+ }
ConsoleHeader.send(this);
diff --git a/src/main/java/gg/nextforge/command/builtin/HologramCommand.java b/src/main/java/gg/nextforge/command/builtin/HologramCommand.java
new file mode 100644
index 0000000..d8af66b
--- /dev/null
+++ b/src/main/java/gg/nextforge/command/builtin/HologramCommand.java
@@ -0,0 +1,156 @@
+package gg.nextforge.command.builtin;
+
+import gg.nextforge.NextCorePlugin;
+import gg.nextforge.command.CommandContext;
+import gg.nextforge.command.CommandManager;
+import gg.nextforge.text.TextManager;
+import gg.nextforge.textblockitemdisplay.*;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Command handling for hologram operations.
+ */
+public class HologramCommand {
+ private final NextCorePlugin plugin;
+ private final HologramManager manager;
+ private final TextManager text;
+
+ public HologramCommand(NextCorePlugin plugin, HologramManager manager) {
+ this.plugin = plugin;
+ this.manager = manager;
+ this.text = plugin.getTextManager();
+ if (plugin.getConfigFile().getBoolean("commands.hologram.enabled", true)) registerCommands();
+ }
+
+ private void registerCommands() {
+ CommandManager cm = plugin.getCommandManager();
+ cm.command("hologram")
+ .permission(plugin.getConfigFile().getString("commands.hologram.permission", "nextforge.command.hologram"))
+ .description("Manage holograms")
+ .executor(this::handleHelp)
+ .subcommand("help", this::handleHelp)
+ .subcommand("list", this::handleList)
+ .subcommand("nearby", this::handleNearby)
+ .subcommand("create", this::handleCreate)
+ .subcommand("remove", this::handleRemove)
+ .subcommand("copy", this::handleCopy)
+ .subcommand("info", this::handleInfo)
+ .register();
+ }
+
+ private void handleHelp(CommandContext ctx) {
+ text.send(ctx.sender(), "/hologram list - list holograms");
+ text.send(ctx.sender(), "/hologram create (type) (name)");
+ }
+
+ private void handleList(CommandContext ctx) {
+ for (Hologram h : manager.getHolograms()) {
+ text.send(ctx.sender(), " - " + h.getName());
+ }
+ }
+
+ private void handleNearby(CommandContext ctx) {
+ if (!(ctx.sender() instanceof Player player)) {
+ text.send(ctx.sender(), "Only players");
+ return;
+ }
+ double range = 10;
+ if (ctx.args().length > 0) {
+ try {
+ range = Double.parseDouble(ctx.args()[0]);
+ } catch (NumberFormatException e) {
+ text.send(ctx.sender(), "Invalid range. Please provide a valid number.");
+ return;
+ }
+ }
+ Location loc = player.getLocation();
+ for (Hologram h : manager.getHolograms()) {
+ if (h.getLocation().getWorld().equals(loc.getWorld()) &&
+ h.getLocation().distance(loc) <= range) {
+ text.send(player, " - " + h.getName());
+ }
+ }
+ }
+
+ private void handleCreate(CommandContext ctx) {
+ if (!(ctx.sender() instanceof Player player)) {
+ text.send(ctx.sender(), "Only players");
+ return;
+ }
+ if (ctx.args().length < 2) {
+ text.send(ctx.sender(), "Usage: /hologram create (type) (name)");
+ return;
+ }
+ String type = ctx.args()[0].toLowerCase();
+ String name = ctx.args()[1];
+ Location loc = player.getLocation();
+ switch (type) {
+ case "text" -> manager.createTextHologram(name, loc);
+ case "item" -> manager.createItemHologram(name, loc, player.getInventory().getItemInMainHand());
+ case "block" -> manager.createBlockHologram(name, loc, Material.STONE);
+ default -> {
+ text.send(ctx.sender(), "Unknown type");
+ return;
+ }
+ }
+ text.send(ctx.sender(), "Created hologram " + name);
+ }
+
+ private void handleRemove(CommandContext ctx) {
+ if (ctx.args().length < 1) {
+ text.send(ctx.sender(), "Usage: /hologram remove (name)");
+ return;
+ }
+ manager.remove(ctx.args()[0]);
+ text.send(ctx.sender(), "Removed hologram");
+ }
+
+ private void handleCopy(CommandContext ctx) {
+ if (ctx.args().length < 2) {
+ text.send(ctx.sender(), "Usage: /hologram copy (src) (dest)");
+ return;
+ }
+ Hologram src = manager.get(ctx.args()[0]);
+ if (src == null) {
+ text.send(ctx.sender(), "Not found");
+ return;
+ }
+ Location loc = src.getLocation();
+ if (src instanceof TextHologram th) {
+ TextHologram nh = manager.createTextHologram(ctx.args()[1], loc);
+ th.getLines().forEach(nh::addLine);
+ } else if (src instanceof ItemHologram ih) {
+ manager.createItemHologram(ctx.args()[1], loc, ih.getItem());
+ } else if (src instanceof BlockHologram bh) {
+ manager.createBlockHologram(ctx.args()[1], loc, bh.getBlockType());
+ }
+ text.send(ctx.sender(), "Copied hologram");
+ }
+
+ private void handleInfo(CommandContext ctx) {
+ if (ctx.args().length < 1) {
+ text.send(ctx.sender(), "Usage: /hologram info (name)");
+ return;
+ }
+ Hologram h = manager.get(ctx.args()[0]);
+ if (h == null) {
+ text.send(ctx.sender(), "Not found");
+ return;
+ }
+ text.send(ctx.sender(), "Name: " + h.getName());
+ text.send(ctx.sender(), "Location: " + h.getLocation().toVector());
+ if (h instanceof TextHologram th) {
+ text.send(ctx.sender(), "Lines: " + th.getLines().size());
+ }
+ if (h instanceof ItemHologram ih) {
+ ItemStack item = ih.getItem();
+ text.send(ctx.sender(), "Item: " + item.getType());
+ }
+ if (h instanceof BlockHologram bh) {
+ text.send(ctx.sender(), "Block: " + bh.getBlockType());
+ }
+ }
+}
diff --git a/src/main/java/gg/nextforge/command/builtin/NPCCommand.java b/src/main/java/gg/nextforge/command/builtin/NPCCommand.java
index 7b87201..549fd94 100644
--- a/src/main/java/gg/nextforge/command/builtin/NPCCommand.java
+++ b/src/main/java/gg/nextforge/command/builtin/NPCCommand.java
@@ -15,10 +15,7 @@
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.stream.Collectors;
public class NPCCommand {
@@ -31,12 +28,12 @@ public NPCCommand(NextCorePlugin plugin, NPCManager npcManager) {
this.plugin = plugin;
this.npcManager = npcManager;
this.textManager = plugin.getTextManager();
- registerCommands();
+ if (plugin.getConfigFile().getBoolean("commands.npc.enabled", true)) registerCommands();
}
private void registerCommands() {
plugin.getCommandManager().command("npc")
- .permission("nextforge.command.npc")
+ .permission(plugin.getConfigFile().getString("commands.npc.permission", "nextforge.command.npc"))
.description("Manage your npc's with ease.")
.aliases("npcs", "npcmanager")
.executor(this::handleHelp)
@@ -133,7 +130,7 @@ private void handleCreate(CommandContext ctx) {
.turnToPlayer(false)
.turnToPlayerDistance(3.0)
.attributes(new java.util.HashMap<>())
- .actions(new java.util.ArrayList<>())
+ .actions(new java.util.HashMap<>())
.location(loc)
.build();
npcManager.register(npc);
@@ -170,7 +167,7 @@ private void handleCopy(CommandContext ctx) {
.turnToPlayer(orig.isTurnToPlayer())
.turnToPlayerDistance(orig.getTurnToPlayerDistance())
.attributes(new java.util.HashMap<>(orig.getAttributes()))
- .actions(new java.util.ArrayList<>(orig.getActions()))
+ .actions(new java.util.HashMap<>(orig.getActions()))
.location(orig.getLocation())
.build();
npcManager.register(copy);
@@ -263,10 +260,12 @@ private void handleInfo(CommandContext ctx) {
if (npc.getActions().isEmpty()) {
textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.info.actions.empty", "│ No actions defined."));
} else {
- for (String action : npc.getActions()) {
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.info.actions.line", "│ %action% (%trigger%)").replace("%action%", action)
- .replace("%trigger%", action.split(" ")[0]) // Assuming the trigger is the first word in the action string
- );
+ for (NPC.ClickType action : npc.getActions().keySet()) {
+ textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.info.actions.line", "│ %action% (%trigger%)").replace("%action%", npc.getActions().get(action).stream()
+ // Aus jedem CommandAction nur den eigentlichen Befehl ziehen
+ .map(NPC.CommandAction::getCommand)
+ .collect(Collectors.joining(", "))
+ .replace("%trigger%", action.name())));
}
}
textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.info.attributes.header", "│ Attributes:"));
@@ -843,55 +842,163 @@ private void handleTeleport(CommandContext ctx) {
}
private void handleAction(CommandContext ctx) {
- if (ctx.args().length < 3) {
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.usage", "%prefix% Usage: /npc action (npc) add|remove|clear|list [params]"));
+ String[] args = ctx.args();
+ // /npc action add|remove|clear|list [...]
+ if (args.length < 2) {
+ textManager.send(ctx.sender(),
+ plugin.getMessagesFile()
+ .getString("commands.npc.action.usage",
+ "%prefix% Usage: /npc action add|remove|clear|list [params]"));
return;
}
- String id = ctx.args()[0];
- String sub = ctx.args()[1];
- NPC npc = npcManager.getNpcs().get(id);
- if (npc == null) { textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.not-found", "%prefix% NPC '%name%' not found.")); return; }
- switch (sub.toLowerCase()) {
- case "list":
+
+ String id = args[0];
+ String sub = args[1].toLowerCase();
+ NPC npc = npcManager.getNpcs().get(id);
+
+ if (npc == null) {
+ textManager.send(ctx.sender(),
+ plugin.getMessagesFile()
+ .getString("commands.npc.action.not-found",
+ "%prefix% NPC '%name%' not found.")
+ .replace("%name%", id));
+ return;
+ }
+
+ switch (sub) {
+ case "list" -> {
if (npc.getActions().isEmpty()) {
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.list.empty", "%prefix% NPC '%name%' has no actions.").replace("%name%", id));
+ textManager.send(ctx.sender(),
+ plugin.getMessagesFile()
+ .getString("commands.npc.action.list.empty",
+ "%prefix% NPC '%name%' has no actions.")
+ .replace("%name%", id));
} else {
- for (String header : plugin.getMessagesFile().getStringList("commands.npc.action.list.header")) {
- header = header.replace("%name%", id);
- textManager.send(ctx.sender(), header);
+ // Header
+ for (String line : plugin.getMessagesFile().getStringList("commands.npc.action.list.header")) {
+ textManager.send(ctx.sender(), line.replace("%name%", id));
}
- for (String action : npc.getActions()) {
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.list.line", "│ %action%").replace("%action%", action));
+ // Jede Click-Type einzeln listen
+ for (NPC.ClickType click : npc.getActions().keySet()) {
+ String joined = npc.getActions()
+ .get(click)
+ .stream()
+ .map(ca -> ca.getActionType().name() + ":" + ca.getCommand())
+ .collect(Collectors.joining(", "));
+ String template = plugin.getMessagesFile()
+ .getString("commands.npc.action.list.line",
+ "│ %trigger% – %action%");
+ String filled = template
+ .replace("%trigger%", click.name())
+ .replace("%action%", joined);
+ textManager.send(ctx.sender(), filled);
}
- for (String footer : plugin.getMessagesFile().getStringList("commands.npc.action.list.footer")) {
- footer = footer.replace("%name%", id);
- textManager.send(ctx.sender(), footer);
+ // Footer
+ for (String line : plugin.getMessagesFile().getStringList("commands.npc.action.list.footer")) {
+ textManager.send(ctx.sender(), line.replace("%name%", id));
}
}
- break;
- case "clear":
- npcManager.modify(id, n -> { n.getActions().clear(); return n; });
- npcManager.save();
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.clear.success", "%prefix% All actions cleared for NPC '%name%'.").replace("%name%", id));
- break;
- case "add":
- String action = ctx.args()[2];
- npcManager.modify(id, n -> { n.getActions().add(action); return n; });
+ }
+
+ case "clear" -> {
+ // Optional: /npc action clear [ClickType]
+ if (args.length == 3) {
+ try {
+ NPC.ClickType click = NPC.ClickType.valueOf(args[2].toUpperCase());
+ npcManager.modify(id, n -> { n.getActions().remove(click); return n; });
+ } catch (IllegalArgumentException ex) {
+ textManager.send(ctx.sender(),
+ "Unknown click type: " + args[2] + "");
+ return;
+ }
+ } else {
+ // Ohne ClickType → alle löschen
+ npcManager.modify(id, n -> { n.getActions().clear(); return n; });
+ }
npcManager.save();
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.add.success", "%prefix% Action '%action%' added to NPC '%name%'.").replace("%action%", action).replace("%name%", id));
- break;
- case "remove":
- String rem = ctx.args()[2];
- npcManager.modify(id, n -> { n.getActions().remove(rem); return n; });
+ textManager.send(ctx.sender(),
+ plugin.getMessagesFile()
+ .getString("commands.npc.action.clear.success",
+ "%prefix% Actions cleared for NPC '%name%'.")
+ .replace("%name%", id));
+ }
+
+ case "add" -> {
+ // /npc action add
+ if (args.length < 5) {
+ textManager.send(ctx.sender(),
+ "Usage: /npc action " + id + " add ");
+ return;
+ }
+ NPC.ClickType click;
+ NPC.ActionType type;
+ try {
+ click = NPC.ClickType.valueOf(args[2].toUpperCase());
+ type = NPC.ActionType.valueOf(args[3].toUpperCase());
+ } catch (IllegalArgumentException ex) {
+ textManager.send(ctx.sender(),
+ "Invalid click- or action-type!");
+ return;
+ }
+ String command = String.join(" ", Arrays.copyOfRange(args, 4, args.length));
+ npcManager.modify(id, n -> {
+ n.getActions()
+ .computeIfAbsent(click, k -> new ArrayList<>())
+ .add(new NPC.CommandAction(type, command));
+ return n;
+ });
npcManager.save();
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.remove.success", "%prefix% Action '%action%' removed from NPC '%name%'.").replace("%action%", rem).replace("%name%", id));
- break;
- default:
- textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.action.usage", "%prefix% Usage: /npc action (npc) add|remove|clear|list [params]"));
- break;
+ textManager.send(ctx.sender(),
+ plugin.getMessagesFile()
+ .getString("commands.npc.action.add.success",
+ "%prefix% Action '%action%' added to NPC '%name%'.")
+ .replace("%action%", type.name() + ":" + command)
+ .replace("%name%", id));
+ }
+
+ case "remove" -> {
+ // /npc action remove
+ if (args.length != 4) {
+ textManager.send(ctx.sender(),
+ "Usage: /npc action " + id + " remove ");
+ return;
+ }
+ NPC.ClickType click;
+ int index;
+ try {
+ click = NPC.ClickType.valueOf(args[2].toUpperCase());
+ index = Integer.parseInt(args[3]);
+ } catch (IllegalArgumentException ex) {
+ textManager.send(ctx.sender(),
+ "Invalid click-type or index!");
+ return;
+ }
+ final boolean removed = npcManager.getNpcs()
+ .get(id)
+ .removeAction(click, index);
+ if (removed) {
+ npcManager.save();
+ textManager.send(ctx.sender(),
+ plugin.getMessagesFile()
+ .getString("commands.npc.action.remove.success",
+ "%prefix% Action removed from NPC '%name%'.")
+ .replace("%name%", id));
+ } else {
+ textManager.send(ctx.sender(),
+ "No action at index " + index + " for " + click.name() + "!");
+ }
+ }
+
+ default -> {
+ textManager.send(ctx.sender(),
+ plugin.getMessagesFile()
+ .getString("commands.npc.action.usage",
+ "%prefix% Usage: /npc action add|remove|clear|list [params]"));
+ }
}
}
+
private void handleInteractionCooldown(CommandContext ctx) {
if (ctx.args().length < 2) {
textManager.send(ctx.sender(), plugin.getMessagesFile().getString("commands.npc.interaction_cooldown.usage", "%prefix% Usage: /npc interaction_cooldown (npc) [disabled | ticks]"));
diff --git a/src/main/java/gg/nextforge/npc/NPCListener.java b/src/main/java/gg/nextforge/npc/NPCListener.java
new file mode 100644
index 0000000..99414a9
--- /dev/null
+++ b/src/main/java/gg/nextforge/npc/NPCListener.java
@@ -0,0 +1,68 @@
+package gg.nextforge.npc;
+
+import gg.nextforge.npc.model.NPC;
+import gg.nextforge.scheduler.CoreScheduler;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+
+public class NPCListener implements Listener {
+ private final NPCManager manager;
+
+ public NPCListener(NPCManager manager) {
+ this.manager = manager;
+ handleTurnToPlayer();
+ }
+
+ @EventHandler
+ public void onPlayerInteractEntity(PlayerInteractEntityEvent e) {
+ NPC npc = manager.getByEntity(e.getRightClicked());
+ if (npc == null) return;
+
+ // any_click + right_click
+ runActions(npc, e.getPlayer(), NPC.ClickType.ANY_CLICK);
+ runActions(npc, e.getPlayer(), NPC.ClickType.RIGHT_CLICK);
+ }
+
+ @EventHandler
+ public void onEntityDamageByEntity(EntityDamageByEntityEvent e) {
+ if (!(e.getDamager() instanceof Player)) return;
+ NPC npc = manager.getByEntity(e.getEntity());
+ if (npc == null) return;
+
+ // any_click + left_click
+ runActions(npc, (Player)e.getDamager(), NPC.ClickType.ANY_CLICK);
+ runActions(npc, (Player)e.getDamager(), NPC.ClickType.LEFT_CLICK);
+ e.setCancelled(true); // Optional: NPC darf keinen Schaden bekommen
+ }
+
+ private void runActions(NPC npc, Player player, NPC.ClickType click) {
+ for (NPC.CommandAction ca : npc.listActions(click)) {
+ switch (ca.getActionType()) {
+ case PLAYER_COMMAND:
+ player.performCommand(ca.getCommand());
+ break;
+ case CONSOLE_COMMAND:
+ Bukkit.dispatchCommand(Bukkit.getConsoleSender(), ca.getCommand());
+ break;
+ }
+ }
+ }
+
+ // Dreh-Task (kann auch als Repeating Task in NPCManager laufen)
+ public void handleTurnToPlayer() {
+ CoreScheduler.runTimer(() -> {
+ for (NPC npc : manager.getNpcs().values()) {
+ if (!npc.isTurnToPlayer()) continue;
+ Player nearest = manager.getNearestPlayer(npc, npc.getTurnToPlayerDistance());
+ if (nearest != null) {
+ manager.rotateNpcHead(npc, nearest.getLocation());
+ }
+ }
+ }, 0L, 2L);
+ }
+}
+
diff --git a/src/main/java/gg/nextforge/npc/NPCManager.java b/src/main/java/gg/nextforge/npc/NPCManager.java
index 03b9347..39abee9 100644
--- a/src/main/java/gg/nextforge/npc/NPCManager.java
+++ b/src/main/java/gg/nextforge/npc/NPCManager.java
@@ -13,14 +13,14 @@
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
+import java.util.*;
import java.util.function.Function;
+import java.util.stream.Collectors;
/**
* Manager class for handling NPCs (Non-Player Characters) in the NextForge framework.
@@ -199,18 +199,30 @@ private NPC loadFromSection(String id, ConfigurationSection sec) {
.scale(sec.getDouble("scale", 1.0))
.transientNPC(sec.getBoolean("transient", false))
.interactionCooldown(sec.getInt("cooldown", 0))
- .turnToPlayer(sec.getBoolean("turnToPlayer"))
+ .turnToPlayer(sec.getBoolean("turnToPlayer", false))
.turnToPlayerDistance(sec.getDouble("turnToPlayerDistance", 3.0))
- .attributes(new HashMap<>())
- .actions(new ArrayList<>())
+ .actions(new EnumMap<>(NPC.ClickType.class))
.build();
- ConfigurationSection attr = sec.getConfigurationSection("attributes");
- if (attr != null) {
- for (String key : attr.getKeys(false)) {
- npc.getAttributes().put(key, attr.getString(key));
+
+ // Load actions
+ ConfigurationSection actionsSec = sec.getConfigurationSection("actions");
+ if (actionsSec != null) {
+ for (String clickKey : actionsSec.getKeys(false)) {
+ try {
+ NPC.ClickType clickType = NPC.ClickType.valueOf(clickKey);
+ List entries = actionsSec.getStringList(clickKey);
+ for (String entry : entries) {
+ String[] parts = entry.split(":", 2);
+ NPC.ActionType type = NPC.ActionType.valueOf(parts[0]);
+ String cmd = parts.length > 1 ? parts[1] : "";
+ npc.getActions().computeIfAbsent(clickType, k -> new ArrayList<>())
+ .add(new NPC.CommandAction(type, cmd));
+ }
+ } catch (IllegalArgumentException ignored) {}
}
}
- npc.getActions().addAll(sec.getStringList("actions"));
+
+ // Load location
ConfigurationSection loc = sec.getConfigurationSection("location");
if (loc != null) {
World w = Bukkit.getWorld(loc.getString("world", "world"));
@@ -260,10 +272,17 @@ private void saveToSection(NPC npc, ConfigurationSection sec) {
sec.set("cooldown", npc.getInteractionCooldown());
sec.set("turnToPlayer", npc.isTurnToPlayer());
sec.set("turnToPlayerDistance", npc.getTurnToPlayerDistance());
- for (String key : npc.getAttributes().keySet()) {
- sec.set("attributes." + key, npc.getAttributes().get(key));
+
+ // Save actions
+ ConfigurationSection actionsSec = sec.createSection("actions");
+ for (Map.Entry> e : npc.getActions().entrySet()) {
+ List list = e.getValue().stream()
+ .map(a -> a.getActionType().name() + ":" + a.getCommand())
+ .collect(Collectors.toList());
+ actionsSec.set(e.getKey().name(), list);
}
- sec.set("actions", npc.getActions());
+
+ // Save location
if (npc.getLocation() != null) {
ConfigurationSection loc = sec.createSection("location");
loc.set("world", npc.getLocation().getWorld().getName());
@@ -274,4 +293,42 @@ private void saveToSection(NPC npc, ConfigurationSection sec) {
loc.set("pitch", npc.getLocation().getPitch());
}
}
+
+ public NPC getByEntity(Entity entity) {
+ if (entity == null || !npcs.containsKey(entity.getUniqueId().toString())) {
+ return null; // Return null if the entity is not an NPC
+ }
+ return npcs.get(entity.getUniqueId().toString()); // Get the NPC by its unique ID
+ }
+
+ public Player getNearestPlayer(NPC npc, double turnToPlayerDistance) {
+ if (npc.getLocation() == null) return null; // Ensure the NPC has a valid location
+ double closestDistance = turnToPlayerDistance;
+ Player closestPlayer = null;
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (player.getWorld().equals(npc.getLocation().getWorld())) {
+ double distance = player.getLocation().distance(npc.getLocation());
+ if (distance <= closestDistance) {
+ closestDistance = distance;
+ closestPlayer = player;
+ }
+ }
+ }
+ return closestPlayer; // Return the nearest player within the specified distance
+ }
+
+ public void rotateNpcHead(NPC npc, @NotNull Location location) {
+ if (npc.getEntity() == null || !(npc.getEntity() instanceof LivingEntity livingEntity)) {
+ return; // Ensure the NPC entity is valid and is a LivingEntity
+ }
+ Location npcLocation = npc.getLocation();
+ if (npcLocation == null) return; // Ensure the NPC has a valid location
+
+ double deltaX = location.getX() - npcLocation.getX();
+ double deltaZ = location.getZ() - npcLocation.getZ();
+ double yaw = Math.toDegrees(Math.atan2(deltaZ, deltaX)) - 90; // Calculate yaw based on the target location
+
+ livingEntity.setRotation((float) yaw, livingEntity.getLocation().getPitch()); // Set the NPC's head rotation
+ }
}
\ No newline at end of file
diff --git a/src/main/java/gg/nextforge/npc/model/NPC.java b/src/main/java/gg/nextforge/npc/model/NPC.java
index 45eb62e..bcc66d8 100644
--- a/src/main/java/gg/nextforge/npc/model/NPC.java
+++ b/src/main/java/gg/nextforge/npc/model/NPC.java
@@ -1,13 +1,13 @@
package gg.nextforge.npc.model;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.Setter;
+import lombok.*;
import org.bukkit.ChatColor;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -32,7 +32,35 @@ public class NPC {
private boolean turnToPlayer;
private double turnToPlayerDistance;
private Map attributes;
- private List actions;
+ private Map> actions;
private Location location;
private Entity entity; // runtime only
+
+ @Getter @Setter @NoArgsConstructor
+ @AllArgsConstructor
+ public static class CommandAction {
+ private ActionType actionType; // PLAYER_COMMAND vs CONSOLE_COMMAND
+ private String command; // Befehl ohne Slash
+ }
+
+ public enum ClickType { ANY_CLICK, LEFT_CLICK, RIGHT_CLICK }
+ public enum ActionType { PLAYER_COMMAND, CONSOLE_COMMAND }
+
+ // Methods für action add/remove/clear/list
+ public void addAction(ClickType click, CommandAction action) {
+ actions.computeIfAbsent(click, k -> new ArrayList<>()).add(action);
+ }
+
+ public boolean removeAction(ClickType click, int index) {
+ List list = actions.get(click);
+ return list != null && list.remove(index) != null;
+ }
+
+ public void clearActions(ClickType click) {
+ actions.remove(click);
+ }
+
+ public List listActions(ClickType click) {
+ return actions.getOrDefault(click, Collections.emptyList());
+ }
}
diff --git a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java
index 5efc684..2a93022 100644
--- a/src/main/java/gg/nextforge/plugin/NextForgePlugin.java
+++ b/src/main/java/gg/nextforge/plugin/NextForgePlugin.java
@@ -7,6 +7,7 @@
import gg.nextforge.protocol.ProtocolManager;
import gg.nextforge.scheduler.CoreScheduler;
import gg.nextforge.text.TextManager;
+import gg.nextforge.textblockitemdisplay.HologramManager;
import lombok.Getter;
import org.bstats.bukkit.Metrics;
import org.bukkit.plugin.Plugin;
@@ -25,6 +26,7 @@ public abstract class NextForgePlugin extends JavaPlugin {
CommandManager commandManager;
TextManager textManager;
NPCManager npcManager;
+ HologramManager hologramManager;
ProtocolManager protocolManager;
Metrics metrics;
@@ -60,6 +62,7 @@ public void onEnable() {
this.commandManager = new CommandManager(this);
this.textManager = new TextManager(this);
this.npcManager = new NPCManager(this);
+ this.hologramManager = new HologramManager();
this.protocolManager = new ProtocolManager(this);
boolean isReload = getServer().getPluginManager().isPluginEnabled("NextForge");
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/BlockHologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/BlockHologram.java
new file mode 100644
index 0000000..49bbf32
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/BlockHologram.java
@@ -0,0 +1,20 @@
+package gg.nextforge.textblockitemdisplay;
+
+import org.bukkit.Material;
+
+/**
+ * Represents a hologram displaying a block.
+ */
+public interface BlockHologram extends Hologram {
+ /**
+ * @return block type displayed by this hologram.
+ */
+ Material getBlockType();
+
+ /**
+ * Sets block type.
+ *
+ * @param material material to display
+ */
+ void setBlockType(Material material);
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/Hologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/Hologram.java
new file mode 100644
index 0000000..b6c32a0
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/Hologram.java
@@ -0,0 +1,42 @@
+package gg.nextforge.textblockitemdisplay;
+
+import org.bukkit.Location;
+
+/**
+ * Base interface for all hologram types.
+ */
+public interface Hologram {
+ /**
+ * @return name of the hologram.
+ */
+ String getName();
+
+ /**
+ * Get the current location of the hologram.
+ *
+ * @return hologram location
+ */
+ Location getLocation();
+
+ /**
+ * Sets the location of this hologram.
+ *
+ * @param location new location
+ */
+ void setLocation(Location location);
+
+ /**
+ * Spawns the hologram.
+ */
+ void spawn();
+
+ /**
+ * Removes the hologram from the world.
+ */
+ void despawn();
+
+ /**
+ * @return whether the hologram is currently spawned
+ */
+ boolean isSpawned();
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/HologramManager.java b/src/main/java/gg/nextforge/textblockitemdisplay/HologramManager.java
new file mode 100644
index 0000000..96c34b7
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/HologramManager.java
@@ -0,0 +1,91 @@
+package gg.nextforge.textblockitemdisplay;
+
+import gg.nextforge.textblockitemdisplay.impl.SimpleBlockHologram;
+import gg.nextforge.textblockitemdisplay.impl.SimpleItemHologram;
+import gg.nextforge.textblockitemdisplay.impl.SimpleTextHologram;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manager responsible for creating and storing holograms.
+ */
+public class HologramManager {
+ private final Map holograms = new ConcurrentHashMap<>();
+
+ /**
+ * Creates a text hologram.
+ *
+ * @param name hologram name
+ * @param location spawn location
+ * @return created hologram
+ */
+ public TextHologram createTextHologram(String name, Location location) {
+ TextHologram holo = new SimpleTextHologram(name, location);
+ holograms.put(name.toLowerCase(), holo);
+ return holo;
+ }
+
+ /**
+ * Creates an item hologram.
+ *
+ * @param name hologram name
+ * @param location spawn location
+ * @param item item to display
+ * @return created hologram
+ */
+ public ItemHologram createItemHologram(String name, Location location, ItemStack item) {
+ ItemHologram holo = new SimpleItemHologram(name, location, item);
+ holograms.put(name.toLowerCase(), holo);
+ return holo;
+ }
+
+ /**
+ * Creates a block hologram.
+ *
+ * @param name hologram name
+ * @param location spawn location
+ * @param material block material
+ * @return created hologram
+ */
+ public BlockHologram createBlockHologram(String name, Location location, Material material) {
+ BlockHologram holo = new SimpleBlockHologram(name, location, material);
+ holograms.put(name.toLowerCase(), holo);
+ return holo;
+ }
+
+ /**
+ * Removes a hologram by name.
+ *
+ * @param name the name of the hologram to remove
+ * If no hologram exists with the given name, no action is taken.
+ */
+ public void remove(String name) {
+ Hologram h = holograms.remove(name.toLowerCase());
+ if (h != null) {
+ h.despawn();
+ }
+ }
+
+ /**
+ * Retrieves a hologram by its name.
+ *
+ * @param name the name of the hologram to retrieve
+ * @return the hologram associated with the given name, or {@code null} if no such hologram exists
+ */
+ public Hologram get(String name) {
+ return holograms.get(name.toLowerCase());
+ }
+
+ /**
+ * @return list of all holograms.
+ */
+ public List getHolograms() {
+ return new ArrayList<>(holograms.values());
+ }
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/ItemHologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/ItemHologram.java
new file mode 100644
index 0000000..4fa8b8f
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/ItemHologram.java
@@ -0,0 +1,20 @@
+package gg.nextforge.textblockitemdisplay;
+
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Represents a hologram displaying an item.
+ */
+public interface ItemHologram extends Hologram {
+ /**
+ * @return item displayed by the hologram.
+ */
+ ItemStack getItem();
+
+ /**
+ * Sets the item to display.
+ *
+ * @param item new item
+ */
+ void setItem(ItemStack item);
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/TextHologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/TextHologram.java
new file mode 100644
index 0000000..6b7750c
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/TextHologram.java
@@ -0,0 +1,35 @@
+package gg.nextforge.textblockitemdisplay;
+
+import java.util.List;
+
+/**
+ * Represents a hologram consisting of text lines.
+ */
+public interface TextHologram extends Hologram {
+ /**
+ * @return immutable list of lines of text.
+ */
+ List getLines();
+
+ /**
+ * Sets a specific line.
+ *
+ * @param index line index
+ * @param text new text
+ */
+ void setLine(int index, String text);
+
+ /**
+ * Adds a line to the end.
+ *
+ * @param text line to add
+ */
+ void addLine(String text);
+
+ /**
+ * Removes a line by index.
+ *
+ * @param index index of line
+ */
+ void removeLine(int index);
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/impl/AbstractHologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/impl/AbstractHologram.java
new file mode 100644
index 0000000..781f30c
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/impl/AbstractHologram.java
@@ -0,0 +1,78 @@
+package gg.nextforge.textblockitemdisplay.impl;
+
+import gg.nextforge.textblockitemdisplay.Hologram;
+import org.bukkit.Location;
+
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Base implementation for holograms providing thread-safe updates.
+ */
+public abstract class AbstractHologram implements Hologram {
+ protected final String name;
+ protected Location location;
+ protected boolean isTransient;
+ private boolean spawned;
+
+ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+ protected AbstractHologram(String name, Location location) {
+ this.name = name;
+ this.location = location.clone();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Location getLocation() {
+ lock.readLock().lock();
+ try {
+ return location.clone();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public void setLocation(Location location) {
+ lock.writeLock().lock();
+ try {
+ this.location = location.clone();
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public void spawn() {
+ lock.writeLock().lock();
+ try {
+ this.spawned = true;
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public void despawn() {
+ lock.writeLock().lock();
+ try {
+ this.spawned = false;
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public boolean isSpawned() {
+ lock.readLock().lock();
+ try {
+ return spawned;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleBlockHologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleBlockHologram.java
new file mode 100644
index 0000000..1ad06cb
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleBlockHologram.java
@@ -0,0 +1,27 @@
+package gg.nextforge.textblockitemdisplay.impl;
+
+import gg.nextforge.textblockitemdisplay.BlockHologram;
+import org.bukkit.Location;
+import org.bukkit.Material;
+
+/**
+ * Basic implementation of a block hologram.
+ */
+public class SimpleBlockHologram extends AbstractHologram implements BlockHologram {
+ private Material material;
+
+ public SimpleBlockHologram(String name, Location location, Material material) {
+ super(name, location);
+ this.material = material;
+ }
+
+ @Override
+ public Material getBlockType() {
+ return material;
+ }
+
+ @Override
+ public void setBlockType(Material material) {
+ this.material = material;
+ }
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleItemHologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleItemHologram.java
new file mode 100644
index 0000000..82b4778
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleItemHologram.java
@@ -0,0 +1,27 @@
+package gg.nextforge.textblockitemdisplay.impl;
+
+import gg.nextforge.textblockitemdisplay.ItemHologram;
+import org.bukkit.Location;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Basic implementation of an item hologram.
+ */
+public class SimpleItemHologram extends AbstractHologram implements ItemHologram {
+ private ItemStack item;
+
+ public SimpleItemHologram(String name, Location location, ItemStack item) {
+ super(name, location);
+ this.item = item.clone();
+ }
+
+ @Override
+ public ItemStack getItem() {
+ return item.clone();
+ }
+
+ @Override
+ public void setItem(ItemStack item) {
+ this.item = item.clone();
+ }
+}
diff --git a/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleTextHologram.java b/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleTextHologram.java
new file mode 100644
index 0000000..fe27b66
--- /dev/null
+++ b/src/main/java/gg/nextforge/textblockitemdisplay/impl/SimpleTextHologram.java
@@ -0,0 +1,47 @@
+package gg.nextforge.textblockitemdisplay.impl;
+
+import gg.nextforge.textblockitemdisplay.TextHologram;
+import org.bukkit.Location;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Basic implementation of a text hologram.
+ */
+public class SimpleTextHologram extends AbstractHologram implements TextHologram {
+ private final List lines = Collections.synchronizedList(new ArrayList<>());
+
+ public SimpleTextHologram(String name, Location location) {
+ super(name, location);
+ }
+
+ @Override
+ public List getLines() {
+ synchronized (lines) {
+ return List.copyOf(lines);
+ }
+ }
+
+ @Override
+ public void setLine(int index, String text) {
+ if (index < 0 || index >= lines.size()) {
+ throw new IllegalArgumentException("Index out of bounds: " + index + ". Valid range is [0, " + (lines.size() - 1) + "].");
+ }
+ lines.set(index, text);
+ }
+
+ @Override
+ public void addLine(String text) {
+ lines.add(text);
+ }
+
+ @Override
+ public void removeLine(int index) {
+ if (index < 0 || index >= lines.size()) {
+ throw new IllegalArgumentException("Index out of bounds: " + index + ". Valid range is [0, " + (lines.size() - 1) + "].");
+ }
+ lines.remove(index);
+ }
+}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 1742ffd..5f76d3f 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -2,4 +2,11 @@ updater:
auto_update: true # Enable automatic updates
check_interval: 7200000 # Check for updates every 2 hours (in milliseconds)
update_branch: 'master' # Branch to check for updates | Available branches: master, dev
- disable_update_message: false # Disable the update message in the console
\ No newline at end of file
+ disable_update_message: false # Disable the update message in the console
+commands:
+ npc:
+ enabled: true # Enable the /npc command
+ permission: 'nextcore.command.npc' # Permission required to use the /npc command
+ hologram:
+ enabled: true # Enable the /hologram command
+ permission: 'nextcore.command.hologram' # Permission required to use the /hologram command
\ No newline at end of file
diff --git a/src/test/java/gg/nextforge/textblockitemdisplay/HologramManagerTest.java b/src/test/java/gg/nextforge/textblockitemdisplay/HologramManagerTest.java
new file mode 100644
index 0000000..6e0e531
--- /dev/null
+++ b/src/test/java/gg/nextforge/textblockitemdisplay/HologramManagerTest.java
@@ -0,0 +1,43 @@
+package gg.nextforge.textblockitemdisplay;
+
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.Material;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for {@link HologramManager}.
+ */
+public class HologramManagerTest {
+
+ private HologramManager manager;
+ private World world;
+
+ @BeforeEach
+ void setUp() {
+ manager = new HologramManager();
+ world = mock(World.class);
+ when(world.getName()).thenReturn("world");
+ }
+
+ @Test
+ void createTextHologram() {
+ Location loc = new Location(world, 0, 0, 0);
+ TextHologram holo = manager.createTextHologram("test", loc);
+ assertNotNull(holo);
+ assertEquals(holo, manager.get("test"));
+ }
+
+ @Test
+ void copyItemHologram() {
+ Location loc = new Location(world, 1, 2, 3);
+ ItemHologram ih = manager.createItemHologram("i", loc, new ItemStack(Material.STONE));
+ manager.createItemHologram("copy", ih.getLocation(), ih.getItem());
+ assertEquals(2, manager.getHolograms().size());
+ }
+}