diff --git a/gradle.properties b/gradle.properties index 405cc86..a363ace 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.20.1 yarn_mappings=1.20.1+build.10 loader_version=0.18.3 # Mod Properties -mod_version=1.0.3 +mod_version=1.0.4 maven_group=dev.tggamesyt archives_base_name=szar # Dependencies diff --git a/src/main/java/dev/tggamesyt/szar/GypsyEntity.java b/src/main/java/dev/tggamesyt/szar/GypsyEntity.java index 4f0aba2..efd27f5 100644 --- a/src/main/java/dev/tggamesyt/szar/GypsyEntity.java +++ b/src/main/java/dev/tggamesyt/szar/GypsyEntity.java @@ -1,6 +1,7 @@ package dev.tggamesyt.szar; -import net.minecraft.entity.EntityType; +import net.minecraft.entity.*; +import net.minecraft.entity.ai.TargetPredicate; import net.minecraft.entity.ai.goal.Goal; import net.minecraft.entity.ai.goal.LookAroundGoal; import net.minecraft.entity.ai.goal.WanderAroundFarGoal; @@ -15,67 +16,67 @@ import net.minecraft.util.collection.DefaultedList; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; -import java.util.EnumSet; -import java.util.List; +import java.util.*; public class GypsyEntity extends PathAwareEntity { private final DefaultedList stolenItems = DefaultedList.of(); - private int stealCooldown = 0; - private boolean fleeing = false; + private final Set stolenFromPlayers = new HashSet<>(); - private static final double FLEE_DISTANCE = 15.0; + private int stealCooldown = 0; + private int panicTicks = 0; + private UUID fleeingFrom = null; + private int heldItemSwapTicks = 0; public GypsyEntity(EntityType type, World world) { super(type, world); + this.setCanPickUpLoot(true); } // ================= ATTRIBUTES ================= - public static DefaultAttributeContainer.Builder createAttributes() { return MobEntity.createMobAttributes() .add(EntityAttributes.GENERIC_MAX_HEALTH, 20.0) - .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.25); + .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.25) + .add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 1.0); } // ================= GOALS ================= - @Override protected void initGoals() { - this.goalSelector.add(0, new FleeWhenSeenGoal(this)); - this.goalSelector.add(1, new SneakBehindPlayerGoal(this)); - this.goalSelector.add(2, new WanderAroundFarGoal(this, 0.8)); - this.goalSelector.add(3, new LookAroundGoal(this)); + this.goalSelector.add(0, new PanicRandomlyGoal(this)); + this.goalSelector.add(1, new FleeSpecificPlayerGoal(this)); + this.goalSelector.add(2, new DefensiveAttackGoal(this)); + this.goalSelector.add(3, new SneakBehindPlayerGoal(this)); + this.goalSelector.add(4, new BiasedWanderGoal(this, 0.6)); + this.goalSelector.add(5, new LookAroundGoal(this)); } // ================= TICK ================= - @Override public void tick() { super.tick(); - if (stealCooldown > 0) { - stealCooldown--; + if (stealCooldown > 0) stealCooldown--; + + if (!this.getWorld().isClient && !stolenItems.isEmpty()) { + heldItemSwapTicks++; + if (heldItemSwapTicks > 60) { + heldItemSwapTicks = 0; + stolenItems.add(stolenItems.remove(0)); + } + equipStack(EquipmentSlot.MAINHAND, stolenItems.get(0)); } } - // ================= VISIBILITY CHECK ================= - - /** - * True if the entity is anywhere on the player's screen (FOV-based) - */ + // ================= VISIBILITY ================= private boolean isOnPlayerScreen(PlayerEntity player) { - if (player.isCreative()) return false; - Vec3d look = player.getRotationVec(1.0F).normalize(); Vec3d toEntity = this.getPos().subtract(player.getEyePos()).normalize(); - - // Rough FOV check (~120° total) - return look.dotProduct(toEntity) > 0.3; + return look.dotProduct(toEntity) > 0.55; } // ================= STEALING ================= - private void trySteal(PlayerEntity player) { if (stealCooldown > 0 || player.isCreative() || this.getWorld().isClient) return; @@ -89,20 +90,37 @@ public class GypsyEntity extends PathAwareEntity { ItemStack stolen = chosen.split(1); stolenItems.add(stolen); - stealCooldown = 20 * 20; // 20 seconds - fleeing = true; + equipStack(EquipmentSlot.MAINHAND, stolen); + stolenFromPlayers.add(player.getUuid()); + fleeingFrom = player.getUuid(); + + stealCooldown = 20 * 20; this.getNavigation().stop(); } - // ================= DAMAGE & LOOT ================= + // ================= ITEM PICKUP (WORLD ITEMS, NOT CRIME) ================= + @Override + protected void loot(ItemEntity item) { + if (this.getWorld().isClient) return; + ItemStack stack = item.getStack(); + if (stack.isEmpty()) return; + ItemStack taken = stack.split(1); + stolenItems.add(taken); + equipStack(EquipmentSlot.MAINHAND, taken); + + if (stack.isEmpty()) item.discard(); + } + + // ================= DAMAGE ================= @Override public boolean damage(DamageSource source, float amount) { boolean result = super.damage(source, amount); - if (!this.getWorld().isClient && !stolenItems.isEmpty()) { - this.dropStack(stolenItems.remove(0)); + if (!this.getWorld().isClient) { + panicTicks = 60 + random.nextInt(40); + if (!stolenItems.isEmpty()) this.dropStack(stolenItems.remove(0)); } return result; @@ -110,108 +128,158 @@ public class GypsyEntity extends PathAwareEntity { @Override protected void dropLoot(DamageSource source, boolean causedByPlayer) { - for (ItemStack stack : stolenItems) { - this.dropStack(stack); - } + for (ItemStack stack : stolenItems) this.dropStack(stack); stolenItems.clear(); } // ================= GOALS ================= - /** - * Runs away when visible OR after stealing, - * stops once far enough away. - */ - private static class FleeWhenSeenGoal extends Goal { - - private final GypsyEntity GypsyEntity; - private PlayerEntity target; - - public FleeWhenSeenGoal(GypsyEntity GypsyEntity) { - this.GypsyEntity = GypsyEntity; - this.setControls(EnumSet.of(Control.MOVE)); - } - - @Override - public boolean canStart() { - this.target = GypsyEntity.getWorld().getClosestPlayer(GypsyEntity, 20); - - if (target == null || target.isCreative()) return false; - - if (GypsyEntity.fleeing) return true; - - return GypsyEntity.isOnPlayerScreen(target); - } - - @Override - public boolean shouldContinue() { - return GypsyEntity.fleeing - && target != null - && GypsyEntity.squaredDistanceTo(target) < FLEE_DISTANCE * FLEE_DISTANCE; - } - - @Override - public void start() { - GypsyEntity.fleeing = true; - moveAway(); - } - + private static class PanicRandomlyGoal extends Goal { + private final GypsyEntity mob; + PanicRandomlyGoal(GypsyEntity mob) { this.mob = mob; this.setControls(EnumSet.of(Control.MOVE)); } + @Override public boolean canStart() { return mob.panicTicks > 0; } @Override public void tick() { - moveAway(); - - if (GypsyEntity.squaredDistanceTo(target) >= FLEE_DISTANCE * FLEE_DISTANCE) { - GypsyEntity.fleeing = false; + mob.panicTicks--; + if (mob.getNavigation().isIdle()) { + Vec3d dest = mob.getPos().add( + mob.random.nextGaussian() * 8, + 0, + mob.random.nextGaussian() * 8 + ); + mob.getNavigation().startMovingTo(dest.x, dest.y, dest.z, 1.5); } } - - private void moveAway() { - Vec3d away = GypsyEntity.getPos() - .subtract(target.getPos()) - .normalize() - .multiply(10); - - Vec3d dest = GypsyEntity.getPos().add(away); - GypsyEntity.getNavigation().startMovingTo(dest.x, dest.y, dest.z, 1.35); - } } - /** - * Sneaks behind players ONLY when unseen and not fleeing - */ - private static class SneakBehindPlayerGoal extends Goal { + // 🔴 Flee from only specific victim, hide behind others + private static class FleeSpecificPlayerGoal extends Goal { + private final GypsyEntity mob; + private PlayerEntity threat; - private final GypsyEntity GypsyEntity; - private PlayerEntity target; - - public SneakBehindPlayerGoal(GypsyEntity GypsyEntity) { - this.GypsyEntity = GypsyEntity; + FleeSpecificPlayerGoal(GypsyEntity mob) { + this.mob = mob; this.setControls(EnumSet.of(Control.MOVE)); } @Override public boolean canStart() { - this.target = GypsyEntity.getWorld().getClosestPlayer(GypsyEntity, 12); - - return target != null - && !target.isCreative() - && !GypsyEntity.isOnPlayerScreen(target) - && GypsyEntity.stealCooldown == 0 - && !GypsyEntity.fleeing; + if (mob.fleeingFrom == null) return false; + PlayerEntity p = mob.getWorld().getPlayerByUuid(mob.fleeingFrom); + if (p == null || !mob.canSee(p) || !mob.isOnPlayerScreen(p)) return false; + threat = p; + return true; } @Override public void tick() { - Vec3d behind = target.getPos() - .subtract(target.getRotationVec(1.0F).normalize().multiply(2)); + TargetPredicate predicate = TargetPredicate.createNonAttackable() + .setBaseMaxDistance(16) + .setPredicate(player -> !player.getUuid().equals(mob.fleeingFrom)); - GypsyEntity.getNavigation().startMovingTo( - behind.x, behind.y, behind.z, 1.0 - ); + PlayerEntity shield = mob.getWorld().getClosestPlayer(predicate, mob); - if (GypsyEntity.distanceTo(target) < 1.5) { - GypsyEntity.trySteal(target); + Vec3d dest; + if (shield != null && !shield.isCreative()) { + dest = shield.getPos(); // hide behind other players + } else { + dest = mob.getPos().subtract(threat.getPos()).normalize().multiply(10).add(mob.getPos()); } + + mob.getNavigation().startMovingTo(dest.x, dest.y, dest.z, 1.3); + } + } + + // ⚔ Attack only the player it stole from + private static class DefensiveAttackGoal extends Goal { + private final GypsyEntity mob; + private PlayerEntity target; + private int cooldown = 0; + + DefensiveAttackGoal(GypsyEntity mob) { this.mob = mob; } + + @Override + public boolean canStart() { + if (mob.fleeingFrom == null) return false; + PlayerEntity p = mob.getWorld().getPlayerByUuid(mob.fleeingFrom); + if (p == null || mob.distanceTo(p) > 1.3) return false; + target = p; + return true; + } + + @Override + public void tick() { + if (cooldown-- > 0) return; + cooldown = 20; + mob.getLookControl().lookAt(target); + mob.tryAttack(target); + } + } + + // 🟡 Sneak steal + private static class SneakBehindPlayerGoal extends Goal { + private final GypsyEntity mob; + private PlayerEntity target; + private int cooldown = 0; + + SneakBehindPlayerGoal(GypsyEntity mob) { + this.mob = mob; + this.setControls(EnumSet.of(Control.MOVE)); + } + + @Override + public boolean canStart() { + target = mob.getWorld().getClosestPlayer(mob, 10); + return target != null + && !mob.stolenFromPlayers.contains(target.getUuid()) + && mob.stealCooldown == 0 + && !mob.isOnPlayerScreen(target) + && !target.isCreative(); + } + + @Override + public void tick() { + if (cooldown-- > 0) return; + cooldown = 5; + + Vec3d behind = target.getPos().subtract(target.getRotationVec(1.0F).normalize()); + mob.getNavigation().startMovingTo(behind.x, behind.y, behind.z, 1.15); + + if (mob.distanceTo(target) < 1.3) { + mob.trySteal(target); + } + } + } + + // 🟢 Biased wander toward players when not guilty + private static class BiasedWanderGoal extends WanderAroundFarGoal { + private final GypsyEntity mob; + + BiasedWanderGoal(GypsyEntity mob, double speed) { + super(mob, speed); + this.mob = mob; + } + + @Override + protected Vec3d getWanderTarget() { + Vec3d base = super.getWanderTarget(); + PlayerEntity player = mob.getWorld().getClosestPlayer(mob, 10); + + if (player == null || base == null || mob.fleeingFrom != null) return base; + + Vec3d best = base; + double bestDist = base.squaredDistanceTo(player.getPos()); + + for (int i = 0; i < 4; i++) { + Vec3d c = super.getWanderTarget(); + if (c == null) continue; + double d = c.squaredDistanceTo(player.getPos()); + if (d < bestDist) { // bias toward player + best = c; + bestDist = d; + } + } + return best; } } }