Volver al índice

src/main/java/ar/com/companeros/Companion.java

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<BlockPos, Integer> blocked = new java.util.HashMap<>();

    public Companion(EntityType<? extends Companion> 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<Item> 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<Item> 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);
        }
    }
}