/* * 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() { } /** * 判断实体是否为合法攻击目标。 * *

判定规则(按优先级): *

* * @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; } }