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:

* */ 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); } }