package com.tiedup.remake.items;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.personality.PacketOpenCommandWandScreen;
import com.tiedup.remake.personality.HomeType;
import com.tiedup.remake.personality.JobExperience;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.NpcNeeds;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.List;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.state.BlockState;
/**
* Command Wand - Used to give commands to collared NPCs.
*
* Personality System Phase E: Command Wand Item
*
*
Usage:
*
* - Right-click on a collared NPC you own to open command GUI
* - Shows personality info, needs, and available commands
* - Only works on NPCs wearing a collar you own
*
*/
public class ItemCommandWand extends Item {
// NBT tags for selection mode
private static final String TAG_SELECTING_FOR = "SelectingFor";
private static final String TAG_JOB_TYPE = "JobType";
// Two-click selection for TRANSFER
private static final String TAG_SELECTING_STEP = "SelectingStep"; // 1 = chest A, 2 = chest B
private static final String TAG_FIRST_CHEST_POS = "FirstChestPos"; // BlockPos of chest A
public ItemCommandWand() {
super(new Item.Properties().stacksTo(1));
}
// ========== Selection Mode Methods ==========
/**
* Check if wand is in selection mode (waiting for chest click).
*/
public static boolean isInSelectionMode(ItemStack stack) {
return stack.hasTag() && stack.getTag().contains(TAG_SELECTING_FOR);
}
/**
* Enter selection mode - waiting for player to click a chest.
*
* HIGH FIX: Now uses UUID instead of entity ID for persistence across restarts.
*
* @param stack The wand item stack
* @param entityUUID The NPC entity UUID (persistent)
* @param job The job command to assign
*/
public static void enterSelectionMode(
ItemStack stack,
java.util.UUID entityUUID,
NpcCommand job
) {
stack.getOrCreateTag().putUUID(TAG_SELECTING_FOR, entityUUID);
stack.getOrCreateTag().putString(TAG_JOB_TYPE, job.name());
// For TRANSFER, we need two clicks
if (job == NpcCommand.TRANSFER) {
stack.getOrCreateTag().putInt(TAG_SELECTING_STEP, 1);
}
}
/**
* Exit selection mode.
*/
public static void exitSelectionMode(ItemStack stack) {
if (stack.hasTag()) {
stack.getTag().remove(TAG_SELECTING_FOR);
stack.getTag().remove(TAG_JOB_TYPE);
stack.getTag().remove(TAG_SELECTING_STEP);
stack.getTag().remove(TAG_FIRST_CHEST_POS);
}
}
/**
* Get the current selection step (1 = first chest, 2 = second chest).
* Returns 0 if not in multi-step selection.
*/
public static int getSelectingStep(ItemStack stack) {
return stack.hasTag() ? stack.getTag().getInt(TAG_SELECTING_STEP) : 0;
}
/**
* Get the first chest position (for TRANSFER).
*/
@Nullable
public static BlockPos getFirstChestPos(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().contains(TAG_FIRST_CHEST_POS)) {
return null;
}
return BlockPos.of(stack.getTag().getLong(TAG_FIRST_CHEST_POS));
}
/**
* Set the first chest position and advance to step 2.
*/
public static void setFirstChestAndAdvance(ItemStack stack, BlockPos pos) {
stack.getOrCreateTag().putLong(TAG_FIRST_CHEST_POS, pos.asLong());
stack.getOrCreateTag().putInt(TAG_SELECTING_STEP, 2);
}
/**
* Get the entity UUID being selected for.
*
* HIGH FIX: Returns UUID instead of entity ID for persistence.
*/
@Nullable
public static java.util.UUID getSelectingForEntity(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().hasUUID(TAG_SELECTING_FOR)) {
return null;
}
return stack.getTag().getUUID(TAG_SELECTING_FOR);
}
/**
* Get the job type being assigned.
*/
@Nullable
public static NpcCommand getSelectingJobType(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().contains(TAG_JOB_TYPE)) {
return null;
}
return NpcCommand.fromString(stack.getTag().getString(TAG_JOB_TYPE));
}
// ========== Block Interaction (Chest Selection) ==========
@Override
public InteractionResult useOn(UseOnContext context) {
ItemStack stack = context.getItemInHand();
Player player = context.getPlayer();
if (player == null) {
return InteractionResult.PASS;
}
BlockPos clickedPos = context.getClickedPos();
Level level = context.getLevel();
BlockState blockState = level.getBlockState(clickedPos);
// Selection mode for job commands
if (!isInSelectionMode(stack)) {
return InteractionResult.PASS;
}
// Must click on a chest
if (!(blockState.getBlock() instanceof ChestBlock)) {
if (!level.isClientSide) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"You must click on a chest to set the work zone!"
);
}
return InteractionResult.FAIL;
}
// Server-side: directly apply the command (we're already on server)
if (!level.isClientSide) {
// HIGH FIX: Use UUID instead of entity ID for persistence
java.util.UUID entityUUID = getSelectingForEntity(stack);
NpcCommand command = getSelectingJobType(stack);
int selectingStep = getSelectingStep(stack);
if (command != null && entityUUID != null) {
// TRANSFER requires two chests
if (command == NpcCommand.TRANSFER) {
if (selectingStep == 1) {
// First click: store source chest A
setFirstChestAndAdvance(stack, clickedPos);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Source chest set at " +
clickedPos.toShortString() +
". Now click on the DESTINATION chest."
);
return InteractionResult.SUCCESS;
} else if (selectingStep == 2) {
// Second click: apply command with both chests
BlockPos sourceChest = getFirstChestPos(stack);
if (sourceChest == null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Error: Source chest not set. Try again."
);
exitSelectionMode(stack);
return InteractionResult.FAIL;
}
// Prevent selecting same chest twice
if (sourceChest.equals(clickedPos)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Destination must be different from source chest!"
);
return InteractionResult.FAIL;
}
// Find the NPC entity (HIGH FIX: lookup by UUID)
net.minecraft.world.entity.Entity entity = (
(net.minecraft.server.level.ServerLevel) level
).getEntity(entityUUID);
if (entity instanceof EntityDamsel damsel) {
// Give TRANSFER command with source (commandTarget) and dest (commandTarget2)
boolean success = damsel.giveCommandWithTwoTargets(
player,
command,
sourceChest, // Source chest A
clickedPos // Destination chest B
);
if (success) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
damsel.getNpcName() +
" will transfer items from " +
sourceChest.toShortString() +
" to " +
clickedPos.toShortString()
);
TiedUpMod.LOGGER.debug(
"[ItemCommandWand] {} set TRANSFER for {} from {} to {}",
player.getName().getString(),
damsel.getNpcName(),
sourceChest,
clickedPos
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
damsel.getNpcName() + " refused the job!"
);
}
}
exitSelectionMode(stack);
return InteractionResult.SUCCESS;
}
}
// Standard single-chest job commands (FARM, COOK, SHEAR, etc.) (HIGH FIX: lookup by UUID)
net.minecraft.world.entity.Entity entity = (
(net.minecraft.server.level.ServerLevel) level
).getEntity(entityUUID);
if (entity instanceof EntityDamsel damsel) {
// Give command directly (already validated acceptance in PacketNpcCommand)
boolean success = damsel.giveCommand(
player,
command,
clickedPos
);
if (success) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Work zone set! " +
damsel.getNpcName() +
" will " +
command.name() +
" at " +
clickedPos.toShortString()
);
TiedUpMod.LOGGER.debug(
"[ItemCommandWand] {} set work zone for {} at {}",
player.getName().getString(),
damsel.getNpcName(),
clickedPos
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
damsel.getNpcName() + " refused the job!"
);
}
}
// Exit selection mode
exitSelectionMode(stack);
}
}
return InteractionResult.SUCCESS;
}
// ========== Entity Interaction ==========
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Only works on EntityDamsel (and subclasses like EntityKidnapper)
if (!(target instanceof EntityDamsel damsel)) {
return InteractionResult.PASS;
}
// Server-side only
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Must have collar
if (!damsel.hasCollar()) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
damsel.getNpcName() + " is not wearing a collar!"
);
return InteractionResult.FAIL;
}
// Get collar and verify ownership
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
if (!(collar.getItem() instanceof ItemCollar collarItem)) {
return InteractionResult.PASS;
}
if (!collarItem.getOwners(collar).contains(player.getUUID())) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"You don't own " + damsel.getNpcName() + "'s collar!"
);
return InteractionResult.FAIL;
}
// Get personality data
PersonalityState state = damsel.getPersonalityState();
if (state == null) {
TiedUpMod.LOGGER.warn(
"[ItemCommandWand] No personality state for {}",
damsel.getNpcName()
);
return InteractionResult.FAIL;
}
// Always show personality (discovery system removed)
String personalityName = state.getPersonality().name();
// Get home type
String homeType = state.getHomeType().name();
// Cell info for GUI
String cellName = "";
String cellQualityName = "";
if (
state.getCellId() != null &&
player.level() instanceof net.minecraft.server.level.ServerLevel sl
) {
CellDataV2 cell = CellRegistryV2.get(sl).getCell(state.getCellId());
if (cell != null) {
cellName =
cell.getName() != null
? cell.getName()
: "Cell " + cell.getId().toString().substring(0, 8);
}
}
cellQualityName =
state.getCellQuality() != null ? state.getCellQuality().name() : "";
// Get needs
NpcNeeds needs = state.getNeeds();
// Get job experience for active command
JobExperience jobExp = state.getJobExperience();
NpcCommand activeCmd = state.getActiveCommand();
String activeJobLevelName = "";
int activeJobXp = 0;
int activeJobXpMax = 10;
if (activeCmd.isActiveJob()) {
JobExperience.JobLevel level = jobExp.getJobLevel(activeCmd);
activeJobLevelName = level.name();
activeJobXp = jobExp.getExperience(activeCmd);
activeJobXpMax = level.maxExp;
}
// Send packet to open GUI (HIGH FIX: pass UUID instead of entity ID)
if (player instanceof ServerPlayer sp) {
ModNetwork.sendToPlayer(
new PacketOpenCommandWandScreen(
damsel.getUUID(),
damsel.getNpcName(),
personalityName,
activeCmd.name(),
needs.getHunger(),
needs.getRest(),
state.getMood(),
state.getFollowDistance().name(),
homeType,
state.isAutoRestEnabled(),
cellName,
cellQualityName,
activeJobLevelName,
activeJobXp,
activeJobXpMax
),
sp
);
}
TiedUpMod.LOGGER.debug(
"[ItemCommandWand] {} opened command wand for {}",
player.getName().getString(),
damsel.getNpcName()
);
return InteractionResult.SUCCESS;
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List tooltip,
TooltipFlag flag
) {
if (isInSelectionMode(stack)) {
NpcCommand job = getSelectingJobType(stack);
int step = getSelectingStep(stack);
tooltip.add(
Component.literal("SELECTION MODE").withStyle(
ChatFormatting.GOLD,
ChatFormatting.BOLD
)
);
// Different messages for TRANSFER two-step selection
if (job == NpcCommand.TRANSFER) {
if (step == 1) {
tooltip.add(
Component.literal("Click SOURCE chest (1/2)").withStyle(
ChatFormatting.YELLOW
)
);
} else if (step == 2) {
BlockPos source = getFirstChestPos(stack);
tooltip.add(
Component.literal(
"Click DESTINATION chest (2/2)"
).withStyle(ChatFormatting.YELLOW)
);
if (source != null) {
tooltip.add(
Component.literal(
"Source: " + source.toShortString()
).withStyle(ChatFormatting.GRAY)
);
}
}
} else {
tooltip.add(
Component.literal(
"Click a chest to set work zone"
).withStyle(ChatFormatting.YELLOW)
);
}
if (job != null) {
tooltip.add(
Component.literal("Job: " + job.name()).withStyle(
ChatFormatting.AQUA
)
);
}
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Right-click empty to cancel").withStyle(
ChatFormatting.RED
)
);
} else {
tooltip.add(
Component.literal("Right-click a collared NPC").withStyle(
ChatFormatting.GRAY
)
);
tooltip.add(
Component.literal("to give commands").withStyle(
ChatFormatting.GRAY
)
);
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Commands: FOLLOW, STAY, HEEL...").withStyle(
ChatFormatting.DARK_PURPLE
)
);
tooltip.add(
Component.literal("Jobs: FARM, COOK, STORE").withStyle(
ChatFormatting.GREEN
)
);
}
}
@Override
public boolean isFoil(ItemStack stack) {
// Enchantment glint when in selection mode
return isInSelectionMode(stack);
}
}