fancynpcs: add turn_to_player_distance and center commands

Co-authored-by: Sadat Sahib <58975768+ssquadteam@users.noreply.github.com>
This commit is contained in:
Oliver
2025-03-29 19:27:17 +01:00
parent ef19a48bc5
commit c9fb39adcd
12 changed files with 228 additions and 14 deletions

View File

@@ -20,6 +20,14 @@ public interface FancyNpcsConfig {
int getNpcUpdateVisibilityInterval();
int getTurnToPlayerDistance();
/**
* Sets the distance at which NPCs turn to the player.
*
* @param distance The new distance value
* @return true if the distance was updated successfully, false otherwise
*/
boolean setTurnToPlayerDistance(int distance);
boolean isTurnToPlayerResetToInitialDirection();

View File

@@ -36,6 +36,7 @@ public class NpcData {
private Consumer<Player> onClick;
private Map<ActionTrigger, List<NpcAction.NpcActionData>> actions;
private boolean turnToPlayer;
private int turnToPlayerDistance = -1; // -1 means use the default from config
private float interactionCooldown;
private float scale;
private int visibilityDistance;
@@ -57,6 +58,7 @@ public class NpcData {
EntityType type,
Map<NpcEquipmentSlot, ItemStack> equipment,
boolean turnToPlayer,
int turnToPlayerDistance,
Consumer<Player> onClick,
Map<ActionTrigger, List<NpcAction.NpcActionData>> actions,
float interactionCooldown,
@@ -81,6 +83,7 @@ public class NpcData {
this.onClick = onClick;
this.actions = actions;
this.turnToPlayer = turnToPlayer;
this.turnToPlayerDistance = turnToPlayerDistance;
this.interactionCooldown = interactionCooldown;
this.scale = scale;
this.visibilityDistance = visibilityDistance;
@@ -108,6 +111,7 @@ public class NpcData {
};
this.actions = new ConcurrentHashMap<>();
this.turnToPlayer = false;
this.turnToPlayerDistance = -1; // Use default from config
this.interactionCooldown = 0;
this.scale = 1;
this.visibilityDistance = -1;
@@ -321,6 +325,27 @@ public class NpcData {
return this;
}
/**
* Gets the turn-to-player distance for this NPC.
*
* @return the custom distance value, or -1 if using the default from config
*/
public int getTurnToPlayerDistance() {
return turnToPlayerDistance;
}
/**
* Sets the turn-to-player distance for this NPC.
*
* @param distance the custom distance value, or -1 to use the default from config
* @return this NpcData instance for method chaining
*/
public NpcData setTurnToPlayerDistance(int distance) {
this.turnToPlayerDistance = distance;
isDirty = true;
return this;
}
public float getInteractionCooldown() {
return interactionCooldown;
}

View File

@@ -94,6 +94,7 @@ public class NpcModifyEvent extends Event implements Cancellable {
SHOW_IN_TAB,
SKIN,
TURN_TO_PLAYER,
TURN_TO_PLAYER_DISTANCE,
TYPE,
// Messages.
MESSAGE_ADD,

View File

@@ -205,6 +205,24 @@ public class FancyNpcsConfigImpl implements FancyNpcsConfig {
return turnToPlayerDistance;
}
@Override
public boolean setTurnToPlayerDistance(int distance) {
// Validate the input - ensure the distance is positive or -1 for default
if (distance <= 0 && distance != -1) {
return false;
}
// Update the config value in memory
this.turnToPlayerDistance = distance;
// Persist to config file
FileConfiguration config = FancyNpcs.getInstance().getConfig();
config.set("turn_to_player_distance", distance);
FancyNpcs.getInstance().saveConfig();
return true;
}
public int getVisibilityDistance() {
return visibilityDistance;
}

View File

@@ -393,6 +393,8 @@ public class NpcManagerImpl implements NpcManager {
}
}
int turnToPlayerDistance = (int) npcConfig.getDouble("npcs." + id + ".turnToPlayerDistance", 0);
NpcData data = new NpcData(
id,
name,
@@ -408,6 +410,7 @@ public class NpcManagerImpl implements NpcManager {
type,
new HashMap<>(),
turnToPlayer,
turnToPlayerDistance,
null,
actions,
interactionCooldown,
@@ -472,4 +475,4 @@ public class NpcManagerImpl implements NpcManager {
logger.error("Could not save backup file for NPCs");
}
}
}
}

View File

@@ -167,6 +167,7 @@ public final class CloudCommandManager {
*/
public @NotNull CloudCommandManager registerCommands() {
annotationParser.parse(AttributeCMD.INSTANCE);
annotationParser.parse(CenterCMD.INSTANCE);
annotationParser.parse(CollidableCMD.INSTANCE);
annotationParser.parse(CopyCMD.INSTANCE);
annotationParser.parse(CreateCMD.INSTANCE);
@@ -187,6 +188,7 @@ public final class CloudCommandManager {
annotationParser.parse(SkinCMD.INSTANCE);
annotationParser.parse(TeleportCMD.INSTANCE);
annotationParser.parse(TurnToPlayerCMD.INSTANCE);
annotationParser.parse(TurnToPlayerDistanceCMD.INSTANCE);
annotationParser.parse(TypeCMD.INSTANCE);
annotationParser.parse(ActionCMD.INSTANCE);
annotationParser.parse(VisibilityDistanceCMD.INSTANCE);

View File

@@ -50,6 +50,7 @@ public enum CopyCMD {
npc.getData().getType(),
new ConcurrentHashMap<>(npc.getData().getEquipment()),
npc.getData().isTurnToPlayer(),
npc.getData().getTurnToPlayerDistance(),
npc.getData().getOnClick(),
new ConcurrentHashMap<>(npc.getData().getActions()),
npc.getData().getInteractionCooldown(),

View File

@@ -7,7 +7,6 @@ import de.oliver.fancynpcs.api.events.NpcModifyEvent;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.Permission;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -23,14 +22,21 @@ public enum TurnToPlayerCMD {
final @NotNull Npc npc,
final @Nullable Boolean state
) {
final boolean finalState = (state == null) ? !npc.getData().isTurnToPlayer() : state;
// Calling the event and updating the state if not cancelled.
if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.TURN_TO_PLAYER, finalState, sender).callEvent()) {
npc.getData().setTurnToPlayer(finalState);
translator.translate(finalState ? "npc_turn_to_player_set_true" : "npc_turn_to_player_set_false").replace("npc", npc.getData().getName()).send(sender);
return;
if (state != null && npc.getData().isTurnToPlayer() != state) {
if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.TURN_TO_PLAYER, state, sender).callEvent()) {
npc.getData().setTurnToPlayer(state);
translator.translate(state ? "npc_turn_to_player_set_true" : "npc_turn_to_player_set_false")
.replace("npc", npc.getData().getName())
.send(sender);
} else {
translator.translate("command_npc_modification_cancelled").send(sender);
}
} else if (state == null) {
// If no state provided, just display current state
boolean currentState = npc.getData().isTurnToPlayer();
translator.translate(currentState ? "npc_turn_to_player_status_true" : "npc_turn_to_player_status_false")
.replace("npc", npc.getData().getName())
.send(sender);
}
translator.translate("command_npc_modification_cancelled").send(sender);
}
}

View File

@@ -16,7 +16,7 @@ public class TurnToPlayerTracker implements Runnable {
@Override
public void run() {
Collection<Npc> npcs = FancyNpcs.getInstance().getNpcManagerImpl().getAllNpcs();
int turnToPlayerDistance = FancyNpcs.getInstance().getFancyNpcConfig().getTurnToPlayerDistance();
int defaultTurnToPlayerDistance = FancyNpcs.getInstance().getFancyNpcConfig().getTurnToPlayerDistance();
for (Player player : Bukkit.getOnlinePlayers()) {
Location playerLocation = player.getLocation();
@@ -33,8 +33,12 @@ public class TurnToPlayerTracker implements Runnable {
if (Double.isNaN(distance)) {
continue;
}
// Get NPC-specific turn distance or fall back to default
int npcTurnDistance = npcData.getTurnToPlayerDistance();
int effectiveTurnDistance = (npcTurnDistance == -1) ? defaultTurnToPlayerDistance : npcTurnDistance;
if (npcData.isTurnToPlayer() && distance < turnToPlayerDistance) {
if (npcData.isTurnToPlayer() && distance < effectiveTurnDistance) {
Location newLoc = playerLocation.clone();
newLoc.setDirection(newLoc.subtract(npcLocation).toVector());
npc.lookAt(player, newLoc);

View File

@@ -107,6 +107,7 @@ messages:
npc_action_set: "<dark_gray> <gray>Syntax: {primaryColor}/npc action {secondaryColor}(npc) (trigger) {primaryColor}set {secondaryColor}(index) (type) [value]"
npc_attribute: "<dark_gray> <gray>Syntax: {primaryColor}/npc attribute {secondaryColor}(npc) {primaryColor}(set | list)"
npc_attribute_set: "<dark_gray> <gray>Syntax: {primaryColor}/npc attribute {secondaryColor}(npc) {primaryColor}set {secondaryColor}(attribute) (value)"
npc_center: "<dark_gray> <gray>Syntax: {primaryColor}/npc center {secondaryColor}(npc)"
npc_collidable: "<dark_gray> <gray>Syntax: {primaryColor}/npc collidable {secondaryColor}(npc) (state)"
npc_copy: "<dark_gray> <gray>Syntax: {primaryColor}/npc copy {secondaryColor}(npc) (new_name)"
npc_create: "<dark_gray> <gray>Syntax: {primaryColor}/npc create {secondaryColor}(npc) [--type] [--position] [--world]"
@@ -125,7 +126,7 @@ messages:
npc_show_in_tab: "<dark_gray> <gray>Syntax: {primaryColor}/npc show_in_tab {secondaryColor}(npc) (state)"
npc_skin: "<dark_gray> <gray>Syntax: {primaryColor}/npc skin {secondaryColor}(npc) (@none | @mirror | name | uuid | placeholder | url | file name) [--slim]"
npc_teleport: "<dark_gray> <gray>Syntax: {primaryColor}/npc teleport {secondaryColor}(npc)"
npc_turn_to_player: "<dark_gray> <gray>Syntax: {primaryColor}/npc turn_to_player {secondaryColor}(npc) (state)"
npc_turn_to_player: "<dark_gray> <gray>Syntax: {primaryColor}/npc turn_to_player {secondaryColor}(npc) (state) [distance]"
npc_type: "<dark_gray> <gray>Syntax: {primaryColor}/npc type {secondaryColor}(npc) (type)"
npc_visibility_distance: "<dark_gray> <gray>Syntax: {primaryColor}/npc visibility_distance {secondaryColor}(npc) (always_visible | default | not_visible | distance)"
@@ -158,6 +159,7 @@ messages:
- "<dark_gray> <hover:show_text:'<gray>Lists all modified attributes of the NPC.'>{primaryColor}/npc attribute {secondaryColor}(npc) {primaryColor}list"
- "<dark_gray> <hover:show_text:'<gray>Changes whether the NPC can collide with other entities.'>{primaryColor}/npc collidable {secondaryColor}(npc) [state]"
- "<dark_gray> <hover:show_text:'<gray>Copies (duplicates) specified NPC.'>{primaryColor}/npc copy {secondaryColor}(npc) (new_name)"
- "<dark_gray> <hover:show_text:'<gray>Centers the NPC on its current block location.'>{primaryColor}/npc center {secondaryColor}(npc)"
- "<dark_gray> <hover:show_text:'<gray>Creates a new NPC. Can be customized with flags.'>{primaryColor}/npc create {secondaryColor}(npc) [--type] [--location] [--world]"
- "<dark_gray> <hover:show_text:'<gray>Changes displayname of the NPC. Supports MiniMessage, PlaceholderAPI and MiniPlaceholders.'>{primaryColor}/npc displayname {secondaryColor}(npc) (name)"
- "<dark_gray> <hover:show_text:'<gray>Sets equipment slot of the NPC to item currently held in main hand, none or a specific item type.'>{primaryColor}/npc equipment {secondaryColor}(npc) {primaryColor}set {secondaryColor}(slot) (@hand | @none | item)"
@@ -175,7 +177,7 @@ messages:
- "<dark_gray> <hover:show_text:'<gray>Changes whether the NPC is shown in the player-list. This works only on NPCs of PLAYER type.<newline><newline>{errorColor}Re-connecting to the server might be required for changes to take effect.'>{primaryColor}/npc show_in_tab {secondaryColor}(npc) (state)"
- "<dark_gray> <hover:show_text:'<gray>Changes skin of the NPC.<newline><gray>Supports PlaceholderAPI and MiniPlaceholders.<newline><newline>{warningColor}@none <dark_gray>- <gray>removes the skin<newline>{warningColor}@mirror <dark_gray>- <gray>mirrors player skin<newline>{warningColor}(name) <dark_gray>- <gray>name of any player<newline>{warningColor}(url) <dark_gray>- <gray>url of the skin texture'>{primaryColor}/npc skin {secondaryColor}(npc) (@none | @mirror | name | url) [--slim]"
- "<dark_gray> <hover:show_text:'<gray>Teleports you to the specified NPC.'>{primaryColor}/npc teleport {secondaryColor}(npc)"
- "<dark_gray> <hover:show_text:'<gray>Changes whether the NPC should turn to the player when in range.'>{primaryColor}/npc turn_to_player {secondaryColor}(npc) (state)"
- "<dark_gray> <hover:show_text:'<gray>Changes whether the NPC should turn to the player when in range. Optionally specify a custom turn distance.'>{primaryColor}/npc turn_to_player {secondaryColor}(npc) (state) [distance]"
- "<dark_gray> <hover:show_text:'<gray>Changes the type of the NPC.'>{primaryColor}/npc type {secondaryColor}(npc) (type)"
- "<dark_gray> <hover:show_text:'<gray>Changes the visibility distance of the NPC.'>{primaryColor}/npc visibility_distance {secondaryColor}(npc) (default | distance | ...)"
@@ -322,6 +324,18 @@ messages:
# Commands (npc turn_to_player)
npc_turn_to_player_set_true: "<dark_gray> <gray>NPC {warningColor}{npc}<gray> is now turning to player."
npc_turn_to_player_set_false: "<dark_gray> <gray>NPC {warningColor}{npc}<gray> is no longer turning to player."
npc_turn_to_player_distance_set: "<dark_gray> <gray>NPC {warningColor}{npc}<gray> will now turn to players within {warningColor}{distance}<gray> blocks."
npc_turn_to_player_distance_default: "<dark_gray> <gray>NPC {warningColor}{npc}<gray> will now use the default distance of {warningColor}{distance}<gray> blocks."
npc_turn_to_player_distance_invalid: "<dark_gray> {errorColor}Invalid distance value. Distance must be -1 (for default) or a positive number."
# Commands (npc type)
npc_type_success: "<dark_gray> <gray>NPC {warningColor}{npc}<gray> type has been changed to {warningColor}{type}<gray>."
# Commands (npc turn_to_player_distance)
turn_to_player_distance_current: "<dark_gray> <gray>Current turn-to-player distance is: {warningColor}{distance} blocks"
turn_to_player_distance_updated: "<dark_gray> {successColor}Turn-to-player distance has been updated to {warningColor}{distance} blocks"
turn_to_player_distance_failed: "<dark_gray> {errorColor}Failed to update turn-to-player distance."
# Commands (npc center)
npc_center_success: "<dark_gray> <gray>NPC {warningColor}{npc}<gray> has been centered to {warningColor}{x}<gray>, {warningColor}{y}<gray>, {warningColor}{z}<gray>."
npc_center_failure_no_location: "<dark_gray> {errorColor}NPC {warningColor}{npc}{errorColor} has no valid location."

View File

@@ -0,0 +1,54 @@
package de.oliver.fancynpcs.commands.npc;
import de.oliver.fancylib.translations.Translator;
import de.oliver.fancynpcs.FancyNpcs;
import de.oliver.fancynpcs.api.Npc;
import de.oliver.fancynpcs.api.NpcData;
import de.oliver.fancynpcs.api.events.NpcModifyEvent;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.Permission;
import org.jetbrains.annotations.NotNull;
public enum CenterCMD {
INSTANCE; // SINGLETON
private final Translator translator = FancyNpcs.getInstance().getTranslator();
@Command("npc center <npc>")
@Permission("fancynpcs.command.npc.center")
public void onCenter(
final @NotNull CommandSender sender,
final @NotNull Npc npc
) {
NpcData npcData = npc.getData();
Location location = npcData.getLocation();
if (location == null) {
translator.translate("npc_center_failure_no_location").replace("npc", npcData.getName()).send(sender);
return;
}
// Center the NPC on the block
Location centeredLocation = location.clone();
centeredLocation.setX(centeredLocation.getBlockX() + 0.5);
centeredLocation.setY(centeredLocation.getY());
centeredLocation.setZ(centeredLocation.getBlockZ() + 0.5);
// Trigger the modify event
if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.LOCATION, centeredLocation, sender).callEvent()) {
npcData.setLocation(centeredLocation);
npc.updateForAll();
translator.translate("npc_center_success")
.replace("npc", npcData.getName())
.replace("x", String.format("%.2f", centeredLocation.getX()))
.replace("y", String.format("%.2f", centeredLocation.getY()))
.replace("z", String.format("%.2f", centeredLocation.getZ()))
.send(sender);
} else {
translator.translate("command_npc_modification_cancelled").send(sender);
}
}
}

View File

@@ -0,0 +1,78 @@
package de.oliver.fancynpcs.commands.npc;
import de.oliver.fancylib.translations.Translator;
import de.oliver.fancynpcs.FancyNpcs;
import de.oliver.fancynpcs.api.Npc;
import de.oliver.fancynpcs.api.events.NpcModifyEvent;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.annotations.Argument;
import org.incendo.cloud.annotations.Command;
import org.incendo.cloud.annotations.Permission;
import org.incendo.cloud.annotations.parser.Parser;
import org.incendo.cloud.annotations.suggestion.Suggestions;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public enum TurnToPlayerDistanceCMD {
INSTANCE;
private final Translator translator = FancyNpcs.getInstance().getTranslator();
// Storing in a static variable to avoid re-creating the array each time suggestion is requested.
private final List<String> DISTANCE_SUGGESTIONS = List.of("default");
@Command("npc turn_to_player_distance <npc> <distance>")
@Permission("fancynpcs.command.npc.turn_to_player_distance")
public void onTurnToPlayerDistance(
final @NotNull CommandSender sender,
final @NotNull Npc npc,
final @Argument(parserName = "TurnToPlayerDistanceCMD/distance") int distance
) {
if (distance < -1) {
translator.translate("npc_turn_to_player_distance_invalid").send(sender);
return;
}
if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.TURN_TO_PLAYER_DISTANCE, distance, sender).callEvent()) {
npc.getData().setTurnToPlayerDistance(distance);
if (distance == -1) {
// Using default distance
int defaultDistance = FancyNpcs.getInstance().getFancyNpcConfig().getTurnToPlayerDistance();
translator.translate("npc_turn_to_player_distance_default")
.replace("npc", npc.getData().getName())
.replace("distance", String.valueOf(defaultDistance))
.send(sender);
} else {
// Using custom distance
translator.translate("npc_turn_to_player_distance_set")
.replace("npc", npc.getData().getName())
.replace("distance", String.valueOf(distance))
.send(sender);
}
} else {
translator.translate("command_npc_modification_cancelled").send(sender);
}
}
/* PARSERS AND SUGGESTIONS */
@Parser(name = "TurnToPlayerDistanceCMD/distance", suggestions = "TurnToPlayerDistanceCMD/distance")
public @NotNull Integer parse(final CommandContext<CommandSender> context, final CommandInput input) {
// If 'default' string is provided, it is being handled as -1.
if (input.peekString().equalsIgnoreCase("default")) {
input.readString();
return -1;
}
// Otherwise, reading next argument as int.
return input.readInteger();
}
@Suggestions("TurnToPlayerDistanceCMD/distance")
public @NotNull List<String> suggest(final CommandContext<CommandSender> context, final CommandInput input) {
return DISTANCE_SUGGESTIONS;
}
}