/*
* AutoStore 模组战斗辅助:自动攻击敌对生物与装备耐久保护。
*/
package autostore.client.combat;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.EndermanEntity;
import net.minecraft.entity.mob.Monster;
import net.minecraft.entity.mob.PiglinEntity;
import net.minecraft.entity.mob.SpiderEntity;
import net.minecraft.entity.mob.ZombifiedPiglinEntity;
import net.minecraft.entity.passive.IronGolemEntity;
import net.minecraft.entity.passive.TameableEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.util.Hand;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
@Environment(EnvType.CLIENT)
/**
* 战斗辅助工具类:自动攻击与耐久检查的纯函数集合。
*
*
目标筛选遵循一条核心原则:只攻击会主动攻击玩家的生物。
* 铁傀儡、僵尸猪灵等中立生物永远不在目标内;末影人/蜘蛛/猪灵等
* "条件敌对"生物只在确实处于主动敌对状态时才被攻击,判定依据
* 与原版 AI 一致且均可客户端验证(激怒标志/眼睛亮度/金盔甲)。
*/
public final class AutoCombat {
/** 原版实体交互距离(格):视线到目标碰撞箱最近点超过该距离无法命中 */
private static final double ATTACK_RANGE_SQUARE = 3.0 * 3.0;
/** 耐久保护检查的装备槽位:主手、副手与四件盔甲 */
private static final EquipmentSlot[] GUARD_SLOTS = {
EquipmentSlot.MAINHAND, EquipmentSlot.OFFHAND,
EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET
};
private AutoCombat() {
}
/**
* 判断实体是否为合法攻击目标。
*
* 判定规则(按优先级):
*
* - 玩家、已驯服宠物:绝不攻击
* - 铁傀儡:不会主动攻击玩家的中立生物,绝不攻击
* - 僵尸猪灵:群体报复机制,绝不攻击
* - 末影人:仅被激怒(瞪视)时主动攻击,此时才反击
* - 蜘蛛:仅黑暗中(眼睛亮度 < 0.5,与原版索敌条件一致)主动攻击
* - 猪灵:玩家穿戴金盔甲时保持中立,未穿戴时才会攻击玩家
* - 其余敌对生物(僵尸/骷髅/苦力怕/卫道士/疣猪兽等):主动攻击
*
*
* @param entity 待判定实体
* @param player 玩家自身
* @return 是否可作为攻击目标
*/
public static boolean isAttackTarget(Entity entity, PlayerEntity player) {
if (!(entity instanceof LivingEntity living)) {
return false;
}
if (living == player || !living.isAlive() || living.isSpectator()) {
return false;
}
// 绝不攻击玩家(含其他真实玩家)
if (living instanceof PlayerEntity) {
return false;
}
// 已驯服的宠物不攻击
if (living instanceof TameableEntity tameable && tameable.isTamed()) {
return false;
}
// 铁傀儡:不会主动攻击玩家的中立生物,绝不攻击
if (living instanceof IronGolemEntity) {
return false;
}
// 僵尸猪灵:激怒一只引来一群,绝不攻击
if (living instanceof ZombifiedPiglinEntity) {
return false;
}
// 末影人:仅被激怒(瞪视/先手攻击)时才主动敌对
if (living instanceof EndermanEntity enderman) {
return enderman.isAngry();
}
// 蜘蛛:与原版一致,仅黑暗中(眼睛亮度 < 0.5)才主动索敌
if (living instanceof SpiderEntity) {
return living.getBrightnessAtEyes() < 0.5f;
}
// 猪灵:玩家穿戴金盔甲时保持中立
if (living instanceof PiglinEntity) {
return !wearsGoldArmor(player);
}
// 其余敌对生物:直接攻击
return living instanceof Monster;
}
/**
* 在索敌范围内查找最近的合法攻击目标(不追击,仅就地战斗)。
*
* @param client 客户端实例
* @param player 玩家
* @param scanRange 索敌半径(格)
* @return 最近的攻击目标,范围内没有时返回 null
*/
public static LivingEntity findTarget(MinecraftClient client, PlayerEntity player, double scanRange) {
if (client == null || client.world == null || player == null) {
return null;
}
Box box = player.getBoundingBox().expand(scanRange);
LivingEntity best = null;
double bestDistance = Double.MAX_VALUE;
for (LivingEntity entity : client.world.getEntitiesByClass(LivingEntity.class, box,
candidate -> isAttackTarget(candidate, player))) {
double distance = eyeToBoxSquared(player.getEyePos(), entity.getBoundingBox());
if (distance < bestDistance) {
bestDistance = distance;
best = entity;
}
}
return best;
}
/**
* 执行一帧攻击:面向目标,在攻击距离内且攻击冷却充满时出手。
* 距离不够只面向不挥击(等 Baritone 寻路或目标靠近),不空挥不追击。
*
* @param client 客户端实例
* @param target 攻击目标
* @return 本帧是否实际发起了攻击
*/
public static boolean tickAttack(MinecraftClient client, LivingEntity target) {
PlayerEntity player = client == null ? null : client.player;
if (player == null || client.interactionManager == null || target == null || !target.isAlive()) {
return false;
}
// 使用物品(如进食/举盾)时不攻击,避免打断
if (player.isSpectator() || player.isUsingItem()) {
return false;
}
// 面向目标:原版服务端不校验朝向,但部分服务器反作弊会校验
faceEntity(player, target);
// 攻击距离校验:视线到目标碰撞箱最近点 ≤ 3 格(原版实体交互距离),够不到不空挥
if (eyeToBoxSquared(player.getEyePos(), target.getBoundingBox()) > ATTACK_RANGE_SQUARE) {
return false;
}
// 攻击冷却充满才出手,保证每次都满蓄力
if (player.getAttackCooldownProgress(0.0f) < 1.0f) {
return false;
}
client.interactionManager.attackEntity(player, target);
player.swingHand(Hand.MAIN_HAND);
player.resetLastAttackedTicks();
return true;
}
/**
* 检查已装备物品(主手/副手/四件盔甲)中是否有耐久低于阈值的。
*
* @param player 玩家
* @param thresholdPercent 耐久阈值(剩余耐久百分比 ≤ 该值视为过低)
* @return 第一个耐久过低的装备槽位,全部正常时返回 null
*/
public static EquipmentSlot findLowDurabilitySlot(PlayerEntity player, int thresholdPercent) {
if (player == null) {
return null;
}
int threshold = Math.max(1, thresholdPercent);
for (EquipmentSlot slot : GUARD_SLOTS) {
ItemStack stack = player.getEquippedStack(slot);
if (stack.isEmpty() || !stack.isDamageable()) {
continue;
}
int maxDamage = stack.getMaxDamage();
int remaining = maxDamage - stack.getDamage();
// 用整数乘法避免浮点误差:剩余百分比 ≤ 阈值
if (remaining * 100 <= threshold * maxDamage) {
return slot;
}
}
return null;
}
/** 返回装备槽位的中文名称(用于日志与聊天提示) */
public static String slotName(EquipmentSlot slot) {
return switch (slot) {
case MAINHAND -> "主手";
case OFFHAND -> "副手";
case HEAD -> "头盔";
case CHEST -> "胸甲";
case LEGS -> "护腿";
case FEET -> "靴子";
case BODY -> "动物盔甲";
};
}
/** 让玩家面向目标(转向目标中躯位置) */
private static void faceEntity(PlayerEntity player, LivingEntity target) {
Vec3d eye = player.getEyePos();
Vec3d aim = target.getPos().add(0.0, target.getHeight() * 0.5, 0.0);
double dx = aim.x - eye.x;
double dy = aim.y - eye.y;
double dz = aim.z - eye.z;
double horizontal = Math.sqrt(dx * dx + dz * dz);
float yaw = (float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0);
float pitch = (float) (-Math.toDegrees(Math.atan2(dy, horizontal)));
player.setYaw(yaw);
player.setPitch(MathHelper.clamp(pitch, -90.0f, 90.0f));
}
/** 计算视线点到包围盒最近点的距离平方(用于原版攻击距离判定) */
private static double eyeToBoxSquared(Vec3d eye, Box box) {
double dx = Math.max(Math.max(box.minX - eye.x, eye.x - box.maxX), 0.0);
double dy = Math.max(Math.max(box.minY - eye.y, eye.y - box.maxY), 0.0);
double dz = Math.max(Math.max(box.minZ - eye.z, eye.z - box.maxZ), 0.0);
return dx * dx + dy * dy + dz * dz;
}
/** 判断玩家四件盔甲中是否至少一件为金质(金盔甲可使猪灵保持中立) */
private static boolean wearsGoldArmor(PlayerEntity player) {
for (EquipmentSlot slot : new EquipmentSlot[]{EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET}) {
ItemStack stack = player.getEquippedStack(slot);
if (!stack.isEmpty() && Registries.ITEM.getId(stack.getItem()).getPath().contains("golden")) {
return true;
}
}
return false;
}
}