update ai of gypsy

This commit is contained in:
2026-01-20 11:26:35 +01:00
parent 83870fc397
commit 5be20ee06a
2 changed files with 179 additions and 111 deletions

View File

@@ -6,7 +6,7 @@ minecraft_version=1.20.1
yarn_mappings=1.20.1+build.10 yarn_mappings=1.20.1+build.10
loader_version=0.18.3 loader_version=0.18.3
# Mod Properties # Mod Properties
mod_version=1.0.3 mod_version=1.0.4
maven_group=dev.tggamesyt maven_group=dev.tggamesyt
archives_base_name=szar archives_base_name=szar
# Dependencies # Dependencies

View File

@@ -1,6 +1,7 @@
package dev.tggamesyt.szar; 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.Goal;
import net.minecraft.entity.ai.goal.LookAroundGoal; import net.minecraft.entity.ai.goal.LookAroundGoal;
import net.minecraft.entity.ai.goal.WanderAroundFarGoal; 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.util.math.Vec3d;
import net.minecraft.world.World; import net.minecraft.world.World;
import java.util.EnumSet; import java.util.*;
import java.util.List;
public class GypsyEntity extends PathAwareEntity { public class GypsyEntity extends PathAwareEntity {
private final DefaultedList<ItemStack> stolenItems = DefaultedList.of(); private final DefaultedList<ItemStack> stolenItems = DefaultedList.of();
private int stealCooldown = 0; private final Set<UUID> stolenFromPlayers = new HashSet<>();
private boolean fleeing = false;
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<? extends PathAwareEntity> type, World world) { public GypsyEntity(EntityType<? extends PathAwareEntity> type, World world) {
super(type, world); super(type, world);
this.setCanPickUpLoot(true);
} }
// ================= ATTRIBUTES ================= // ================= ATTRIBUTES =================
public static DefaultAttributeContainer.Builder createAttributes() { public static DefaultAttributeContainer.Builder createAttributes() {
return MobEntity.createMobAttributes() return MobEntity.createMobAttributes()
.add(EntityAttributes.GENERIC_MAX_HEALTH, 20.0) .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 ================= // ================= GOALS =================
@Override @Override
protected void initGoals() { protected void initGoals() {
this.goalSelector.add(0, new FleeWhenSeenGoal(this)); this.goalSelector.add(0, new PanicRandomlyGoal(this));
this.goalSelector.add(1, new SneakBehindPlayerGoal(this)); this.goalSelector.add(1, new FleeSpecificPlayerGoal(this));
this.goalSelector.add(2, new WanderAroundFarGoal(this, 0.8)); this.goalSelector.add(2, new DefensiveAttackGoal(this));
this.goalSelector.add(3, new LookAroundGoal(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 ================= // ================= TICK =================
@Override @Override
public void tick() { public void tick() {
super.tick(); super.tick();
if (stealCooldown > 0) { if (stealCooldown > 0) stealCooldown--;
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 ================= // ================= VISIBILITY =================
/**
* True if the entity is anywhere on the player's screen (FOV-based)
*/
private boolean isOnPlayerScreen(PlayerEntity player) { private boolean isOnPlayerScreen(PlayerEntity player) {
if (player.isCreative()) return false;
Vec3d look = player.getRotationVec(1.0F).normalize(); Vec3d look = player.getRotationVec(1.0F).normalize();
Vec3d toEntity = this.getPos().subtract(player.getEyePos()).normalize(); Vec3d toEntity = this.getPos().subtract(player.getEyePos()).normalize();
return look.dotProduct(toEntity) > 0.55;
// Rough FOV check (~120° total)
return look.dotProduct(toEntity) > 0.3;
} }
// ================= STEALING ================= // ================= STEALING =================
private void trySteal(PlayerEntity player) { private void trySteal(PlayerEntity player) {
if (stealCooldown > 0 || player.isCreative() || this.getWorld().isClient) return; if (stealCooldown > 0 || player.isCreative() || this.getWorld().isClient) return;
@@ -89,20 +90,37 @@ public class GypsyEntity extends PathAwareEntity {
ItemStack stolen = chosen.split(1); ItemStack stolen = chosen.split(1);
stolenItems.add(stolen); stolenItems.add(stolen);
stealCooldown = 20 * 20; // 20 seconds equipStack(EquipmentSlot.MAINHAND, stolen);
fleeing = true;
stolenFromPlayers.add(player.getUuid());
fleeingFrom = player.getUuid();
stealCooldown = 20 * 20;
this.getNavigation().stop(); 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 @Override
public boolean damage(DamageSource source, float amount) { public boolean damage(DamageSource source, float amount) {
boolean result = super.damage(source, amount); boolean result = super.damage(source, amount);
if (!this.getWorld().isClient && !stolenItems.isEmpty()) { if (!this.getWorld().isClient) {
this.dropStack(stolenItems.remove(0)); panicTicks = 60 + random.nextInt(40);
if (!stolenItems.isEmpty()) this.dropStack(stolenItems.remove(0));
} }
return result; return result;
@@ -110,108 +128,158 @@ public class GypsyEntity extends PathAwareEntity {
@Override @Override
protected void dropLoot(DamageSource source, boolean causedByPlayer) { protected void dropLoot(DamageSource source, boolean causedByPlayer) {
for (ItemStack stack : stolenItems) { for (ItemStack stack : stolenItems) this.dropStack(stack);
this.dropStack(stack);
}
stolenItems.clear(); stolenItems.clear();
} }
// ================= GOALS ================= // ================= GOALS =================
/** private static class PanicRandomlyGoal extends Goal {
* Runs away when visible OR after stealing, private final GypsyEntity mob;
* stops once far enough away. PanicRandomlyGoal(GypsyEntity mob) { this.mob = mob; this.setControls(EnumSet.of(Control.MOVE)); }
*/ @Override public boolean canStart() { return mob.panicTicks > 0; }
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();
}
@Override @Override
public void tick() { public void tick() {
moveAway(); mob.panicTicks--;
if (mob.getNavigation().isIdle()) {
if (GypsyEntity.squaredDistanceTo(target) >= FLEE_DISTANCE * FLEE_DISTANCE) { Vec3d dest = mob.getPos().add(
GypsyEntity.fleeing = false; mob.random.nextGaussian() * 8,
} 0,
} mob.random.nextGaussian() * 8
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 {
private final GypsyEntity GypsyEntity;
private PlayerEntity target;
public SneakBehindPlayerGoal(GypsyEntity GypsyEntity) {
this.GypsyEntity = GypsyEntity;
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;
}
@Override
public void tick() {
Vec3d behind = target.getPos()
.subtract(target.getRotationVec(1.0F).normalize().multiply(2));
GypsyEntity.getNavigation().startMovingTo(
behind.x, behind.y, behind.z, 1.0
); );
mob.getNavigation().startMovingTo(dest.x, dest.y, dest.z, 1.5);
}
}
}
if (GypsyEntity.distanceTo(target) < 1.5) { // 🔴 Flee from only specific victim, hide behind others
GypsyEntity.trySteal(target); private static class FleeSpecificPlayerGoal extends Goal {
private final GypsyEntity mob;
private PlayerEntity threat;
FleeSpecificPlayerGoal(GypsyEntity mob) {
this.mob = mob;
this.setControls(EnumSet.of(Control.MOVE));
}
@Override
public boolean canStart() {
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() {
TargetPredicate predicate = TargetPredicate.createNonAttackable()
.setBaseMaxDistance(16)
.setPredicate(player -> !player.getUuid().equals(mob.fleeingFrom));
PlayerEntity shield = mob.getWorld().getClosestPlayer(predicate, mob);
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;
}
}
} }