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