Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
538 lines
20 KiB
Java
538 lines
20 KiB
Java
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
|
|
*
|
|
* <p><b>Usage:</b></p>
|
|
* <ul>
|
|
* <li>Right-click on a collared NPC you own to open command GUI</li>
|
|
* <li>Shows personality info, needs, and available commands</li>
|
|
* <li>Only works on NPCs wearing a collar you own</li>
|
|
* </ul>
|
|
*/
|
|
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<Component> 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);
|
|
}
|
|
}
|