package ar.com.companeros; import java.util.UUID; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.ItemTags; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.SimpleContainer; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.PathfinderMob; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.goal.FloatGoal; import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.monster.Creeper; import net.minecraft.world.entity.monster.Monster; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.TieredItem; import net.minecraft.world.level.Level; /** Cuerpo del compañero, inventario y necesidades; todo el progreso se guarda en NBT. */ public class Companion extends PathfinderMob { public final SimpleContainer bag = new SimpleContainer(36); public BlockPos home; public UUID owner; public int radius = 24; public int hunger = 20; public String mode = "autonomo"; public String activity = "Explorando"; public boolean building = false; public int shelterStep = 0; public BlockPos resource; public float miningProgress; public int searchCursor; public int workTicks; public int survivalTicks; public final java.util.Map blocked = new java.util.HashMap<>(); public Companion(EntityType type, Level level) { super(type, level); setPersistenceRequired(); setCanPickUpLoot(false); if (getNavigation() instanceof net.minecraft.world.entity.ai.navigation.GroundPathNavigation ground) ground.setCanOpenDoors(true); for (EquipmentSlot slot : EquipmentSlot.values()) setDropChance(slot, 0); } public static AttributeSupplier.Builder attributes() { return createMobAttributes().add(Attributes.MAX_HEALTH, 20) .add(Attributes.MOVEMENT_SPEED, 0.28).add(Attributes.FOLLOW_RANGE, 32) .add(Attributes.ATTACK_DAMAGE, 1); } // Prioridades de movimiento: agua, combate, trabajo y mirada. @Override protected void registerGoals() { goalSelector.addGoal(0, new FloatGoal(this)); goalSelector.addGoal(0, new net.minecraft.world.entity.ai.goal.OpenDoorGoal(this, true)); goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.1, true)); goalSelector.addGoal(2, new Brain(this)); goalSelector.addGoal(5, new LookAtPlayerGoal(this, Player.class, 8)); goalSelector.addGoal(6, new RandomLookAroundGoal(this)); targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Monster.class, 10, true, false, entity -> !(entity instanceof Creeper) && entity.distanceToSqr(this) < 100 && getHealth() > 7 && !mode.equals("quieto") && humanPresent())); } public ServerLevel serverLevel() { return (ServerLevel) level(); } @Override public void onAddedToWorld() { super.onAddedToWorld(); if (!level().isClientSide && owner != null) { WorkArea.get(serverLevel()).register(getUUID()); var registry = ar.com.companeros.identity.AgentRegistry.get(serverLevel().getServer()); var previous = registry.find(getUUID()).orElse(null); String name = previous == null ? getName().getString() : previous.name(); boolean conflict = previous == null && registry.entries().stream().anyMatch(identity -> identity.state() != ar.com.companeros.identity.AgentState.RETIRED && identity.name().equalsIgnoreCase(getName().getString())); if (!name.matches("[A-Za-z0-9_]{1,16}") || conflict || name.equalsIgnoreCase("all")) { name = "Comp_" + getUUID().toString().substring(0, 8); setCustomName(net.minecraft.network.chat.Component.literal(name)); com.mojang.logging.LogUtils.getLogger().info("Identidad importada del compañero {}: {}", getUUID(), name); } registry.register(getUUID(), name, owner, home == null ? null : net.minecraft.core.GlobalPos.of(level().dimension(), home), mode); } } @Override public boolean canChangeDimensions() { return false; } public boolean humanPresent() { return level() instanceof ServerLevel server && server.players().stream() .anyMatch(ar.com.companeros.identity.AgentRegistry.get(server.getServer())::isHuman); } public boolean inArea(BlockPos pos) { return home != null && Math.abs(pos.getX() - home.getX()) <= radius && Math.abs(pos.getZ() - home.getZ()) <= radius && Math.abs(pos.getY() - home.getY()) <= 12; } // Comida, recuperación, recogida de objetos y huida de creepers. @Override public void aiStep() { if (!level().isClientSide && !humanPresent()) { setTarget(null); getNavigation().stop(); } super.aiStep(); if (level().isClientSide || !isAlive() || !humanPresent()) return; survivalTicks++; if (survivalTicks % 1200 == 0) hunger = Math.max(0, hunger - 1); if (survivalTicks % 80 == 0) { if (hunger == 0 && getHealth() > 1) hurt(damageSources().starve(), 1); if (hunger >= 18 && getHealth() < getMaxHealth()) { heal(1); hunger--; } } if (hunger <= 16) eat(); if (survivalTicks % 20 == 0) equipArmor(); if (getTarget() != null) equipBest(ItemTags.SWORDS); if (survivalTicks % 10 == 0) { for (ItemEntity item : serverLevel().getEntitiesOfClass(ItemEntity.class, getBoundingBox().inflate(1.3), e -> e.isAlive() && !e.hasPickUpDelay() && inArea(e.blockPosition()))) { ItemStack original = item.getItem(); // No recoge objetos arrojados por una persona ni botines ajenos. if (item.getOwner() != null && !item.getOwner().getUUID().equals(getUUID())) continue; CompoundTag provenance = item.saveWithoutId(new CompoundTag()); if (provenance.hasUUID("Thrower") && !provenance.getUUID("Thrower").equals(getUUID())) continue; if (provenance.hasUUID("Owner") && !provenance.getUUID("Owner").equals(getUUID())) continue; int before = original.getCount(); ItemStack remaining = bag.addItem(original.copy()); if (remaining.getCount() == before) continue; take(item, before - remaining.getCount()); if (remaining.isEmpty()) item.discard(); else item.setItem(remaining); } } Creeper danger = serverLevel().getEntitiesOfClass(Creeper.class, getBoundingBox().inflate(6)) .stream().findFirst().orElse(null); if (danger != null || (getHealth() <= 7 && getTarget() != null)) { setTarget(null); var away = position().subtract(danger != null ? danger.position() : home == null ? position() : net.minecraft.world.phys.Vec3.atCenterOf(home)); if (away.lengthSqr() < 0.01) away = new net.minecraft.world.phys.Vec3(1, 0, 0); var flee = position().add(away.normalize().scale(6)); getNavigation().moveTo(flee.x, getY(), flee.z, 1.3); activity = "Buscando un lugar seguro"; } } public boolean eat() { for (int i = 0; i < bag.getContainerSize(); i++) { ItemStack stack = bag.getItem(i); var food = stack.getFoodProperties(this); if (food == null || stack.is(Items.ROTTEN_FLESH) || stack.is(Items.POISONOUS_POTATO) || stack.is(Items.SPIDER_EYE) || stack.is(Items.PUFFERFISH)) continue; hunger = Math.min(20, hunger + food.getNutrition()); ItemStack remainder = stack.copyWithCount(1).finishUsingItem(level(), this); stack.shrink(1); if (!remainder.isEmpty()) bag.addItem(remainder); return true; } return false; } public int count(Item item) { int result = 0; for (int i = 0; i < bag.getContainerSize(); i++) if (bag.getItem(i).is(item)) result += bag.getItem(i).getCount(); return result; } public int count(net.minecraft.tags.TagKey tag) { int result = 0; for (int i = 0; i < bag.getContainerSize(); i++) if (bag.getItem(i).is(tag)) result += bag.getItem(i).getCount(); return result; } public void equipBest(net.minecraft.tags.TagKey tag) { ItemStack best = ItemStack.EMPTY; for (int i = 0; i < bag.getContainerSize(); i++) { ItemStack stack = bag.getItem(i); if (stack.is(tag) && (best.isEmpty() || toolTier(stack) > toolTier(best))) best = stack; } setItemSlot(EquipmentSlot.MAINHAND, best.copy()); } private float toolTier(ItemStack stack) { return stack.getItem() instanceof TieredItem tool ? tool.getTier().getLevel() : 0; } private void equipArmor() { for (EquipmentSlot slot : EquipmentSlot.values()) { if (slot.getType() != EquipmentSlot.Type.ARMOR) continue; ItemStack best = ItemStack.EMPTY; int defense = -1; for (int i = 0; i < bag.getContainerSize(); i++) { ItemStack stack = bag.getItem(i); if (stack.getItem() instanceof net.minecraft.world.item.ArmorItem armor && armor.getEquipmentSlot() == slot && armor.getDefense() > defense) { best = stack; defense = armor.getDefense(); } } setItemSlot(slot, best.copy()); } } @Override protected void hurtArmor(DamageSource source, float damage) { for (int i = 0; i < bag.getContainerSize(); i++) { ItemStack stack = bag.getItem(i); if (stack.getItem() instanceof net.minecraft.world.item.ArmorItem armor && ItemStack.isSameItemSameTags(stack, getItemBySlot(armor.getEquipmentSlot()))) { EquipmentSlot slot = armor.getEquipmentSlot(); stack.hurtAndBreak(Math.max(1, (int) damage / 4), this, entity -> entity.broadcastBreakEvent(slot)); setItemSlot(slot, stack.copy()); } } } public ItemStack toolFor(net.minecraft.world.level.block.state.BlockState state) { ItemStack best = ItemStack.EMPTY; float speed = 1; for (int i = 0; i < bag.getContainerSize(); i++) { ItemStack stack = bag.getItem(i); float candidate = stack.getDestroySpeed(state); if (candidate > speed) { best = stack; speed = candidate; } } setItemSlot(EquipmentSlot.MAINHAND, best.copy()); return best; } @Override public boolean doHurtTarget(net.minecraft.world.entity.Entity target) { boolean hit = super.doHurtTarget(target); if (hit) for (int i = 0; i < bag.getContainerSize(); i++) { ItemStack stack = bag.getItem(i); if (!stack.isEmpty() && stack.is(getMainHandItem().getItem())) { stack.hurtAndBreak(1, this, entity -> entity.broadcastBreakEvent(InteractionHand.MAIN_HAND)); setItemSlot(EquipmentSlot.MAINHAND, stack.copy()); break; } } return hit; } // Interacción: el dueño puede entregar objetos con clic derecho. @Override protected InteractionResult mobInteract(Player player, InteractionHand hand) { if (owner == null || !owner.equals(player.getUUID())) return InteractionResult.PASS; if (!level().isClientSide) { ItemStack held = player.getItemInHand(hand); if (!held.isEmpty()) { ItemStack remaining = bag.addItem(held.copy()); held.setCount(remaining.getCount()); } else player.sendSystemMessage(Component.literal(getName().getString() + ": " + activity + " · Salud " + (int) getHealth() + "/20 · Comida " + hunger + "/20")); } return InteractionResult.sidedSuccess(level().isClientSide); } // Persistencia y muerte: sin duplicar el equipo visual que representa el inventario. @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.put("Bag", bag.createTag()); if (home != null) tag.putLong("Home", home.asLong()); if (owner != null) tag.putUUID("Owner", owner); tag.putInt("Radius", radius); tag.putInt("Hunger", hunger); tag.putString("Mode", mode); tag.putBoolean("Building", building); tag.putInt("ShelterStep", shelterStep); tag.putInt("SurvivalTicks", survivalTicks); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); bag.clearContent(); bag.fromTag(tag.getList("Bag", 10)); home = tag.contains("Home") ? BlockPos.of(tag.getLong("Home")) : null; owner = tag.hasUUID("Owner") ? tag.getUUID("Owner") : null; radius = Math.max(8, Math.min(32, tag.getInt("Radius"))); hunger = Math.max(0, Math.min(20, tag.getInt("Hunger"))); mode = tag.getString("Mode"); building = tag.getBoolean("Building"); shelterStep = Math.max(0, tag.getInt("ShelterStep")); survivalTicks = Math.max(0, tag.getInt("SurvivalTicks")); } @Override protected void dropCustomDeathLoot(DamageSource source, int looting, boolean hit) { super.dropCustomDeathLoot(source, looting, hit); WorkArea.get(serverLevel()).forget(getUUID()); releaseInventory(); } public void releaseInventory() { for (int i = 0; i < bag.getContainerSize(); i++) { ItemStack stack = bag.removeItemNoUpdate(i); if (!stack.isEmpty()) spawnAtLocation(stack); } } }