diff --git a/src/main/java/cc/fros/features/modules/combat/CrystalAuraModule.java b/src/main/java/cc/fros/features/modules/combat/CrystalAuraModule.java new file mode 100644 index 0000000..e222479 --- /dev/null +++ b/src/main/java/cc/fros/features/modules/combat/CrystalAuraModule.java @@ -0,0 +1,929 @@ +package cc.fros.features.modules.combat; + +import cc.fros.Fros; +import cc.fros.event.impl.entity.player.TickEvent; +import cc.fros.event.impl.render.Render3DEvent; +import cc.fros.event.system.Subscribe; +import cc.fros.features.modules.Module; +import cc.fros.features.settings.Setting; +import cc.fros.util.inventory.InventoryUtil; +import cc.fros.util.inventory.ResultType; +import cc.fros.util.models.Angles; +import cc.fros.util.models.Timer; +import cc.fros.util.render.RenderUtil; +import cc.fros.util.world.BlockUtil; +import cc.fros.util.world.CrystalUtil; +import cc.fros.util.world.TargetUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.boss.enderdragon.EndCrystal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RespawnAnchorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.awt.*; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * CrystalAura -- places and breaks end crystals to kill players. + * Ported from telaviv's {@code CatAura}. + */ +public class CrystalAuraModule extends Module { + // ---- Calc ---- + private final Setting page = mode("Page", Page.Calc); + private final Setting sort = mode("Sort", Sort.Damage); + private final Setting targetRange = num("TargetRange", 7.0, 3.0, 20.0); + private final Setting minDmg = num("MinDamage", 2.0, 0.0, 36.0); + private final Setting maxSelfDmg = num("MaxSelfDamage", 4.0, 0.0, 36.0); + private final Setting noSuicide = bool("AntiSuicide", false); + private final Setting lethalCrystals = num("LethalCrystals", 0, 0, 5); + private final Setting terrain = bool("Terrain", false); + private final Setting multiTask = mode("MultiTask", MultiTask.None); + + // ---- Place ---- + private final Setting placeMode = mode("PlaceMode", PlaceMode.Normal); + private final Setting placeRange = num("PlaceRange", 6.0, 1.0, 6.0); + private final Setting placeWallsRange = num("PlaceWallsRange", 3.0, 1.0, 6.0); + private final Setting placeDelay = num("PlaceDelay", 1, 0, 1000); + private final Setting autoSwitch = mode("AutoSwitch", AutoSwitch.Normal); + private final Setting pause = bool("Pause", false); + private final Setting strictDirection = bool("StrictDirection", false); + private final Setting onePointTwelve = bool("1.12", false); + + // ---- AutoSurroundAnchor ---- + private final Setting chargeDelay = num("ChargeDelay", 2, 0, 20); + private final Setting explodeDelay = num("ExplodeDelay", 1, 0, 20); + + // ---- Break ---- + private final Setting breakRange = num("BreakRange", 6.0, 1.0, 6.0); + private final Setting breakWallsRange = num("BreakWallsRange", 3.0, 1.0, 6.0); + private final Setting breakDelay = num("BreakDelay", 1, 0, 1000); + private final Setting ticksExisted = num("TicksExisted", 0, 0, 10); + private final Setting inhibit = bool("Inhibit", false); + private final Setting antiWeakness = mode("AntiWeakness", AntiWeakness.None); + + // ---- Misc ---- + private final Setting rotate = bool("Rotate", false); + private final Setting rotationType = mode("RotationType", RotationType.Simple); + + // ---- Render ---- + private final Setting fillColor = color("FillColor", 0, 0, 0, 100); + private final Setting lineColor = color("OutlineColor", 255, 255, 255, 255); + + private final Timer placeTimer = new Timer(); + private final Timer breakTimer = new Timer(); + + public final Map hitCrystals = new ConcurrentHashMap<>(); + + private Player target; + private double displayDmg; + private boolean offhand; + private boolean placeIsSupport; + + // AutoSurroundAnchor state + private enum AnchorState { IDLE, BREAKING, PLACING_ANCHOR, CHARGING, EXPLODING } + private AnchorState anchorState = AnchorState.IDLE; + private BlockPos anchorPos; + private int anchorTimer; + private int lastSlot; + + public BlockPos renderPos; + private BlockPos calcPos; + private BlockPos lastPos; + private Angles placeRots; + private Angles breakRots; + private Entity calcCrystal; + private double renderDmg; + + public CrystalAuraModule() { + super("CrystalAura", "Places and breaks crystals to kill players.", Category.COMBAT); + refreshVisibility(); + } + + private void refreshVisibility() { + sort.setVisibility(v -> on(Page.Calc)); + targetRange.setVisibility(v -> on(Page.Calc)); + minDmg.setVisibility(v -> on(Page.Calc)); + maxSelfDmg.setVisibility(v -> on(Page.Calc)); + noSuicide.setVisibility(v -> on(Page.Calc)); + lethalCrystals.setVisibility(v -> on(Page.Calc)); + terrain.setVisibility(v -> on(Page.Calc)); + multiTask.setVisibility(v -> on(Page.Calc)); + + placeMode.setVisibility(v -> on(Page.Place)); + placeRange.setVisibility(v -> on(Page.Place)); + placeWallsRange.setVisibility(v -> on(Page.Place)); + placeDelay.setVisibility(v -> on(Page.Place)); + autoSwitch.setVisibility(v -> on(Page.Place)); + pause.setVisibility(v -> on(Page.Place) && autoSwitch.getValue() == AutoSwitch.Normal); + strictDirection.setVisibility(v -> on(Page.Place)); + onePointTwelve.setVisibility(v -> on(Page.Place)); + + chargeDelay.setVisibility(v -> on(Page.Place) && placeMode.getValue() == PlaceMode.AutoSurroundAnchor); + explodeDelay.setVisibility(v -> on(Page.Place) && placeMode.getValue() == PlaceMode.AutoSurroundAnchor); + + breakRange.setVisibility(v -> on(Page.Break)); + breakWallsRange.setVisibility(v -> on(Page.Break)); + breakDelay.setVisibility(v -> on(Page.Break)); + ticksExisted.setVisibility(v -> on(Page.Break)); + inhibit.setVisibility(v -> on(Page.Break)); + antiWeakness.setVisibility(v -> on(Page.Break)); + + rotate.setVisibility(v -> on(Page.Misc)); + rotationType.setVisibility(v -> on(Page.Misc) && rotate.getValue()); + + fillColor.setVisibility(v -> on(Page.Render)); + lineColor.setVisibility(v -> on(Page.Render)); + } + + private boolean on(Page p) { + return page.getValue() == p; + } + + @Override + public void onEnable() { + resetState(); + placeTimer.reset(); + breakTimer.reset(); + } + + @Override + public void onDisable() { + resetState(); + } + + private void resetState() { + renderPos = null; + target = null; + displayDmg = 0; + calcPos = null; + calcCrystal = null; + placeRots = null; + breakRots = null; + lastPos = null; + placeIsSupport = false; + + anchorState = AnchorState.IDLE; + anchorPos = null; + anchorTimer = 0; + lastSlot = -1; + } + + @Subscribe + public void onPreTick(TickEvent.Pre event) { + if (nullCheck() || !mc.player.isAlive()) return; + + if (isMultiTask()) { + resetState(); + return; + } + + prepare(); + + if (rotate.getValue() && actionRots() != null) { + if (rotationType.getValue() == RotationType.Silent) { + Fros.rotationManager.silent(actionRots(), this::interact); + } else { + Fros.rotationManager.motion(actionRots(), 0, this::interact); + } + } else { + interact(); + } + } + + private Angles actionRots() { + if (calcCrystal != null && breakRots != null) return breakRots; + return placeRots; + } + + private boolean isMultiTask() { + if (!mc.player.isUsingItem()) return false; + if (multiTask.getValue() == MultiTask.Strong) return true; + if (multiTask.getValue() == MultiTask.Soft) { + return mc.player.getUsedItemHand() != InteractionHand.OFF_HAND && !offhand; + } + return false; + } + + // ------------------------------------------------------------------ + // Target / calculation + // ------------------------------------------------------------------ + + private void prepare() { + offhand = mc.player.getOffhandItem().getItem() == Items.END_CRYSTAL; + + calcPos = null; + calcCrystal = null; + placeRots = null; + breakRots = null; + + // Handle AutoSurroundAnchor mode + if (placeMode.getValue() == PlaceMode.AutoSurroundAnchor) { + handleAutoSurroundAnchor(); + return; + } + + Result result = getTargetResult(); + if (result == null) return; + + load(result); + + if (inhibit.getValue() && calcCrystal != null && hitCrystals.containsKey(calcCrystal.getId())) { + breakTimer.setMs(breakDelay.getValue().longValue() + 100L); + } + } + + private void load(Result result) { + target = result.target; + displayDmg = result.getDamage(); + if (crystalSlot() != -1 || offhand) { + calcPos = result.calcPos; + placeRots = result.placeRots; + renderDmg = result.bestPlaceDmg; + placeIsSupport = result.placeIsSupport; + if (calcPos == null && renderDmg == 0) renderPos = null; + } else { + calcPos = null; + placeRots = null; + renderDmg = 0; + placeIsSupport = false; + } + calcCrystal = result.calcCrystal; + breakRots = result.breakRots; + } + + // ------------------------------------------------------------------ + // AutoSurroundAnchor - Break surround, place anchor, charge, explode + // ------------------------------------------------------------------ + private void handleAutoSurroundAnchor() { + if (target == null || !target.isAlive() || target.isRemoved()) { + LivingEntity t = TargetUtil.getTarget(targetRange.getValue()); + if (t instanceof Player p) target = p; + else { + resetState(); + return; + } + } + + // State machine for anchor sequence + switch (anchorState) { + case IDLE -> { + // Find the block below target (their feet block) + BlockPos footPos = target.blockPosition().below(); + if (!canBreakSurroundBlock(footPos)) { + // Try to find a breakable surround block around them + footPos = findBreakableSurroundBlock(target); + if (footPos == null) return; + } + anchorPos = footPos; + anchorState = AnchorState.BREAKING; + anchorTimer = 0; + // Re-find target each cycle + LivingEntity t = TargetUtil.getTarget(targetRange.getValue()); + if (t instanceof Player p) target = p; + else return; + } + case BREAKING -> { + if (!breakSurroundBlock(anchorPos)) return; + // Block broken, place anchor next + anchorState = AnchorState.PLACING_ANCHOR; + anchorTimer = 0; + } + case PLACING_ANCHOR -> { + if (!placeAnchor(anchorPos)) return; + anchorState = AnchorState.CHARGING; + anchorTimer = 0; + } + case CHARGING -> { + if (!chargeAnchor(anchorPos)) return; + anchorState = AnchorState.EXPLODING; + anchorTimer = 0; + } + case EXPLODING -> { + if (!explodeAnchor(anchorPos)) return; + // Sequence complete + anchorState = AnchorState.IDLE; + anchorPos = null; + anchorTimer = 0; + } + } + } + + private boolean canBreakSurroundBlock(BlockPos pos) { + if (mc.level.getBlockState(pos).isAir()) return false; + // Check if we can reach and break this block + double distance = mc.player.getEyePosition().distanceTo(Vec3.atCenterOf(pos)); + if (distance > breakRange.getValue()) return false; + // Can't break bedrock + if (mc.level.getBlockState(pos).getBlock() == Blocks.BEDROCK) return false; + return true; + } + + private BlockPos findBreakableSurroundBlock(Player target) { + // Check blocks around the target (surround positions) + for (Direction dir : new Direction[]{Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST}) { + BlockPos pos = target.blockPosition().relative(dir); + if (canBreakSurroundBlock(pos)) return pos; + } + return null; + } + + private boolean breakSurroundBlock(BlockPos pos) { + if (anchorTimer++ < 1) return false; // Wait 1 tick + + BlockState state = mc.level.getBlockState(pos); + if (state.isAir()) { + // Already broken + return true; + } + + // Break the block + Direction side = BlockUtil.getDirection(pos, false); + if (side == null) return false; + + int oldSlot = InventoryUtil.selected(); + int pickaxeSlot = InventoryUtil.find(stack -> stack.is(ItemTags.PICKAXES), InventoryUtil.HOTBAR_SCOPE).slot(); + if (pickaxeSlot == -1) pickaxeSlot = oldSlot; + + InventoryUtil.swap(pickaxeSlot); + mc.gameMode.destroyBlock(pos); + mc.player.swing(InteractionHand.MAIN_HAND); + InventoryUtil.swap(oldSlot); + + return true; + } + + private boolean placeAnchor(BlockPos pos) { + if (anchorTimer++ < 1) return false; // Wait 1 tick + + // Check if block is now air + if (!mc.level.getBlockState(pos).isAir()) return false; + + // Find respawn anchor in hotbar + int anchorSlot = InventoryUtil.find(Items.RESPAWN_ANCHOR, InventoryUtil.HOTBAR_SCOPE).slot(); + if (anchorSlot == -1) return false; + + int oldSlot = InventoryUtil.selected(); + lastSlot = oldSlot; + + InventoryUtil.swap(anchorSlot); + Direction side = BlockUtil.getPlaceableSideCrystal(pos, strictDirection.getValue()); + if (side == null) { + InventoryUtil.swap(oldSlot); + return false; + } + + Vec3 vec = pos.getCenter().add(side.getStepX() * 0.5, side.getStepY() * 0.5, side.getStepZ() * 0.5); + if (side.getAxis().isHorizontal()) vec = vec.add(0, 0.45, 0); + + BlockHitResult hit = new BlockHitResult(vec, side, pos, false); + mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, hit); + mc.player.swing(InteractionHand.MAIN_HAND); + + // Swap back silently + InventoryUtil.swap(oldSlot); + + return true; + } + + private boolean chargeAnchor(BlockPos pos) { + if (anchorTimer++ < chargeDelay.getValue()) return false; // Configurable delay + + // Check anchor exists and get charges + BlockState state = mc.level.getBlockState(pos); + if (state.getBlock() != Blocks.RESPAWN_ANCHOR) return false; + + int charges = state.getValue(RespawnAnchorBlock.CHARGE); + if (charges > 0) { + // Already charged, ready to explode + anchorTimer = 0; + return true; + } + + // Need glowstone + int glowstoneSlot = InventoryUtil.find(Items.GLOWSTONE, InventoryUtil.HOTBAR_SCOPE).slot(); + if (glowstoneSlot == -1) return false; + + int oldSlot = InventoryUtil.selected(); + InventoryUtil.swap(glowstoneSlot); + + // Right-click the anchor to charge + Vec3 center = Vec3.atCenterOf(pos).add(0, 1.0, 0); // Anchor hitbox center + Direction side = BlockUtil.getDirection(pos, false); + if (side == null) side = Direction.UP; + BlockHitResult hit = new BlockHitResult(center, side, pos, true); + + mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, hit); + mc.player.swing(InteractionHand.MAIN_HAND); + + InventoryUtil.swap(oldSlot); + anchorTimer = 0; + return true; + } + + private boolean explodeAnchor(BlockPos pos) { + if (anchorTimer++ < explodeDelay.getValue()) return false; // Configurable delay + + // Check anchor exists and is charged + BlockState state = mc.level.getBlockState(pos); + if (state.getBlock() != Blocks.RESPAWN_ANCHOR) return false; + + int charges = state.getValue(RespawnAnchorBlock.CHARGE); + if (charges == 0) return false; + + // Find any non-glowstone item + int itemSlot = InventoryUtil.find(stack -> !stack.is(Items.GLOWSTONE), InventoryUtil.HOTBAR_SCOPE).slot(); + if (itemSlot == -1) itemSlot = lastSlot; + if (itemSlot == -1) return false; + + int oldSlot = InventoryUtil.selected(); + InventoryUtil.swap(itemSlot); + + // Right-click the anchor to explode + Vec3 center = Vec3.atCenterOf(pos).add(0, 1.0, 0); + Direction side = BlockUtil.getDirection(pos, false); + if (side == null) side = Direction.UP; + BlockHitResult hit = new BlockHitResult(center, side, pos, true); + + mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, hit); + mc.player.swing(InteractionHand.MAIN_HAND); + + InventoryUtil.swap(oldSlot); + anchorTimer = 0; + return true; + } + + private Result getTargetResult() { + if (sort.getValue() == Sort.Range) { + LivingEntity t = TargetUtil.getTarget(targetRange.getValue()); + if (t == null) { + resetState(); + return null; + } + return getResult((Player) t); + } + + List targets = TargetUtil.getTargets(targetRange.getValue()); + Result best = null; + for (Entity entity : targets) { + Result current = getResult((Player) entity); + if (best == null || current.getDamage() > best.getDamage()) best = current; + } + if (best == null) { + resetState(); + return null; + } + return best; + } + + private Result getResult(Player player) { + BreakResult breakResult = getBestBreakResult(player); + return new Result(player, breakResult, getBestPlaceResult(player, breakResult.calcCrystal)); + } + + private BreakResult getBestBreakResult(Player targetPlayer) { + Entity bestCrystal = null; + double bestDamage = 0.5; + + for (Entity entity : mc.level.entitiesForRendering()) { + if (!(entity instanceof EndCrystal crystal)) continue; + if (!canBreakCrystal(crystal)) continue; + + float targetDamage = CrystalUtil.calculateDamage(targetPlayer, entity.position(), terrain.getValue()); + boolean lethal = lethalCrystals.getValue() != 0 + && targetDamage * lethalCrystals.getValue() >= targetPlayer.getHealth() + targetPlayer.getAbsorptionAmount(); + + if (!(bestDamage < targetDamage && targetDamage > minDmg.getValue()) && !lethal) continue; + + float selfDamage = CrystalUtil.calculateDamage(mc.player, entity.position(), terrain.getValue()); + if (selfDamage > maxSelfDmg.getValue()) continue; + if (selfDamage + 2 > mc.player.getHealth() + mc.player.getAbsorptionAmount() && noSuicide.getValue()) continue; + + bestDamage = targetDamage; + bestCrystal = entity; + } + + Angles rots = null; + if (bestCrystal != null && rotate.getValue()) { + rots = Angles.pos(new Vec3(bestCrystal.getX(), bestCrystal.getEyeY(), bestCrystal.getZ())); + } + return new BreakResult(bestDamage, bestCrystal, rots); + } + + private PlaceResult getBestPlaceResult(Player targetPlayer, Entity currentCalcCrystal) { + BlockPos bestPos = null; + double bestDamage = 0.5; + boolean isSupport = false; + + List sphere = BlockUtil.sphere(placeRange.getValue() + 1.0, mc.player.blockPosition(), true, false); + for (BlockPos pos : sphere) { + boolean hasBlock = CrystalUtil.canPlaceCrystal(pos, onePointTwelve.getValue()); + boolean canAirPlace = placeMode.getValue() != PlaceMode.Normal && CrystalUtil.canPlaceCrystalAir(pos); + boolean canSupport = placeMode.getValue() == PlaceMode.Support && mc.level.getBlockState(pos).canBeReplaced(); + + if (!hasBlock && !canAirPlace && !canSupport) continue; + if (!handleBBCrystal(pos, lastPos, currentCalcCrystal)) continue; + if (!checkPlace(pos)) continue; + + // For support placements, we need air above the support block + if (canSupport) { + BlockPos up = pos.above(); + if (!mc.level.getBlockState(up).isAir()) continue; + if (onePointTwelve.getValue() && !mc.level.getBlockState(up.above()).isAir()) continue; + } + + float targetDamage = CrystalUtil.calculateDamage(targetPlayer, crystalDamageVec(pos), terrain.getValue()); + if (!(bestDamage < targetDamage && targetDamage > minDmg.getValue())) continue; + + float selfDamage = CrystalUtil.calculateDamage(mc.player, crystalDamageVec(pos), terrain.getValue()); + if (selfDamage > maxSelfDmg.getValue()) continue; + if (selfDamage + 2 > mc.player.getHealth() + mc.player.getAbsorptionAmount() && noSuicide.getValue()) continue; + + boolean isSupportCandidate = canSupport || (canAirPlace && !hasBlock); + if (targetDamage > bestDamage || (isSupportCandidate && !isSupport)) { + bestDamage = targetDamage; + bestPos = pos; + isSupport = isSupportCandidate; + } + } + + if (bestDamage == 0.5) bestDamage = 0; + return new PlaceResult(bestDamage, bestPos, getPlaceRot(bestPos), isSupport); + } + + private boolean handleBBCrystal(BlockPos pos, BlockPos last, Entity currentCalcCrystal) { + AABB bb = new AABB(pos.above()); + bb = new AABB(bb.minX, bb.minY, bb.minZ, bb.minX + 1.0, bb.minY + 2.0, bb.minZ + 1.0); + + for (Entity entity : mc.level.getEntitiesOfClass(Entity.class, bb, e -> !e.isSpectator())) { + if (entity instanceof EndCrystal) { + if (!entity.isAlive() || entity.blockPosition().below().equals(pos) || entity.equals(currentCalcCrystal)) continue; + } + if (!entity.isAlive() || entity.equals(currentCalcCrystal)) continue; + return false; + } + return true; + } + + private boolean checkPlace(BlockPos pos) { + Direction side = BlockUtil.getPlaceableSideCrystal(pos, strictDirection.getValue()); + if (side == null) return false; + + Vec3 vec = pos.getCenter().add(side.getStepX() * 0.5, side.getStepY() * 0.5, side.getStepZ() * 0.5); + double distanceSq = vec.distanceToSqr(mc.player.getX(), mc.player.getY(), mc.player.getZ()); + if (distanceSq > square(placeRange.getValue())) return false; + + boolean trace = BlockUtil.placeTrace(pos); + if (trace && distanceSq > square(placeWallsRange.getValue())) return false; + + double crystalDistance = mc.player.getEyePosition().distanceTo(pos.getCenter().add(0, 2.20000004768, 0)); + if (crystalDistance > breakRange.getValue()) return false; + + Vec3 raytrace = pos.getCenter().add(0, 2.20000004768, 0); + if (crystalDistance > breakWallsRange.getValue()) { + BlockHitResult result = mc.level.clip(new ClipContext( + mc.player.getEyePosition(), raytrace, + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, mc.player)); + return result.getType() == HitResult.Type.MISS; + } + return true; + } + + private Angles getPlaceRot(BlockPos pos) { + if (pos == null || !rotate.getValue()) return null; + Direction side = BlockUtil.getPlaceableSideCrystal(pos, strictDirection.getValue()); + if (side == null) return null; + Vec3 vec = pos.getCenter().add(side.getStepX() * 0.5, side.getStepY() * 0.5, side.getStepZ() * 0.5); + if (side.getAxis().isHorizontal()) vec = vec.add(0, 0.45, 0); + return Angles.pos(vec); + } + + private Vec3 crystalDamageVec(BlockPos pos) { + return Vec3.atLowerCornerOf(pos).add(0.5, 1.0, 0.5); + } + + // ------------------------------------------------------------------ + // Actions + // ------------------------------------------------------------------ + + public void interact() { + breakCrystal(); + if (crystalSlot() != -1 || offhand) { + if (placeIsSupport) { + placeSupportBlock(); + } else { + placeCrystal(); + } + } + } + + private boolean breakCrystal() { + if (calcCrystal == null) return false; + if (rotate.getValue() && breakRots == null) return false; + if (!breakTimer.passedMs(breakDelay.getValue().longValue())) return false; + + int currentSlot = InventoryUtil.selected(); + int weaponSlot = -1; + boolean silentWeapon = false; + + if (antiWeakness.getValue() != AntiWeakness.None && isWeakened()) { + weaponSlot = swordSlot(); + if (weaponSlot != -1 && currentSlot != weaponSlot) { + if (antiWeakness.getValue() == AntiWeakness.Silent) { + // Packet-only: the server sees the sword for the attack while the client keeps + // mining with whatever is actually selected. + silentSelect(weaponSlot); + silentWeapon = true; + } else { + // Normal: real switch, stays on the weapon. + InventoryUtil.swap(weaponSlot); + } + } + } + + mc.gameMode.attack(mc.player, calcCrystal); + mc.player.swing(offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND); + mc.player.resetAttackStrengthTicker(); + hitCrystals.put(calcCrystal.getId(), System.currentTimeMillis()); + + if (silentWeapon) silentRestore(); + + breakTimer.reset(); + return true; + } + + private void placeCrystal() { + if (calcPos == null) return; + if (rotate.getValue() && placeRots == null) return; + if (!placeTimer.passedMs(placeDelay.getValue().longValue())) return; + + int crystalSlot = crystalSlot(); + if (crystalSlot == -1 && !offhand) { + renderPos = null; + return; + } + + int oldSlot = InventoryUtil.selected(); + boolean silent = false; + + if (!offhand && autoSwitch.getValue() == AutoSwitch.Normal) { + if (!pause.getValue() || !isEatingGap()) InventoryUtil.swap(crystalSlot); + } else if (!offhand && autoSwitch.getValue() == AutoSwitch.Silent) { + // Packet-only switch: the server is told we hold crystals while the client keeps its + // own selected slot, so mining with the tool in hand is never interrupted. + if (oldSlot != crystalSlot) { + silentSelect(crystalSlot); + silent = true; + } + } + + if (autoSwitch.getValue() == AutoSwitch.None + && mc.player.getMainHandItem().getItem() != Items.END_CRYSTAL && !offhand) { + renderPos = null; + return; + } + + Direction side = BlockUtil.getPlaceableSideCrystal(calcPos, strictDirection.getValue()); + if (side != null) { + Vec3 vec = calcPos.getCenter().add(side.getStepX() * 0.5, side.getStepY() * 0.5, side.getStepZ() * 0.5); + if (side.getAxis().isHorizontal()) vec = vec.add(0, 0.45, 0); + + BlockHitResult hit = new BlockHitResult(vec, side, calcPos, false); + mc.gameMode.useItemOn(mc.player, offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND, hit); + mc.player.swing(offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND); + + lastPos = calcPos; + placeTimer.reset(); + } + + // Restore on every path: previously the swap-back lived inside the side != null block, so a + // failed placement left the server stuck on the crystal slot. + if (silent) silentRestore(); + + renderPos = calcPos; + } + + private void placeSupportBlock() { + if (calcPos == null) return; + if (rotate.getValue() && placeRots == null) return; + if (!placeTimer.passedMs(placeDelay.getValue().longValue())) return; + + int obsidianSlot = InventoryUtil.find(Items.OBSIDIAN, InventoryUtil.HOTBAR_SCOPE).slot(); + int endStoneSlot = InventoryUtil.find(Items.END_STONE, InventoryUtil.HOTBAR_SCOPE).slot(); + int supportSlot = obsidianSlot != -1 ? obsidianSlot : endStoneSlot; + + if (supportSlot == -1) { + renderPos = null; + return; + } + + int oldSlot = InventoryUtil.selected(); + boolean silent = false; + + if (autoSwitch.getValue() == AutoSwitch.Normal) { + if (!pause.getValue() || !isEatingGap()) InventoryUtil.swap(supportSlot); + } else if (autoSwitch.getValue() == AutoSwitch.Silent) { + // Packet-only switch, same as placeCrystal: the client keeps its tool in hand. + if (oldSlot != supportSlot) { + silentSelect(supportSlot); + silent = true; + } + } + + if (autoSwitch.getValue() == AutoSwitch.None + && mc.player.getMainHandItem().getItem() != Items.OBSIDIAN + && mc.player.getMainHandItem().getItem() != Items.END_STONE) { + renderPos = null; + return; + } + + Direction side = BlockUtil.getPlaceableSideCrystal(calcPos, strictDirection.getValue()); + if (side != null) { + Vec3 vec = calcPos.getCenter().add(side.getStepX() * 0.5, side.getStepY() * 0.5, side.getStepZ() * 0.5); + if (side.getAxis().isHorizontal()) vec = vec.add(0, 0.45, 0); + + BlockHitResult hit = new BlockHitResult(vec, side, calcPos, false); + mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, hit); + mc.player.swing(InteractionHand.MAIN_HAND); + + lastPos = calcPos; + placeTimer.reset(); + + // After placing support block, try to place crystal on next tick + placeIsSupport = false; + } + + // Restore on every path, including failed placements. + if (silent) silentRestore(); + + renderPos = calcPos; + } + + private boolean canBreakCrystal(EndCrystal crystal) { + double distance = mc.player.getEyePosition().distanceTo(crystal.position().add(0, 1.700000047683716, 0)); + if (rotationType.getValue() == RotationType.GrimAbuse && crystal.blockPosition().getY() > mc.player.blockPosition().getY()) { + distance = mc.player.getEyePosition().distanceTo(crystal.position().add(0, 0.5, 0)); + } + if (ticksExisted.getValue() != 0 && crystal.tickCount <= ticksExisted.getValue()) return false; + if (distance > breakRange.getValue()) return false; + return canEntityBeSeen(crystal) || distance <= breakWallsRange.getValue(); + } + + private boolean canEntityBeSeen(Entity entity) { + Vec3 entityPos = new Vec3(entity.getX(), entity.getEyeY(), entity.getZ()); + if (entity instanceof EndCrystal) { + if (canVecBeSeen(entity.position().add(0, 1.700000047683716, 0))) return true; + } + return canVecBeSeen(entityPos); + } + + private boolean canVecBeSeen(Vec3 vec) { + BlockHitResult result = mc.level.clip(new ClipContext( + mc.player.getEyePosition(), vec, + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, mc.player)); + return result.getType() == HitResult.Type.MISS; + } + + private boolean isWeakened() { + return mc.player.getEffect(MobEffects.WEAKNESS) != null; + } + + private boolean isEatingGap() { + ItemStack stack = mc.player.getMainHandItem(); + return (stack.getItem() == Items.GOLDEN_APPLE || stack.getItem() == Items.ENCHANTED_GOLDEN_APPLE) + && mc.player.isUsingItem(); + } + + private int crystalSlot() { + cc.fros.util.inventory.Result result = InventoryUtil.find(Items.END_CRYSTAL, InventoryUtil.HOTBAR_SCOPE); + if (!result.found()) return -1; + if (result.type() == ResultType.OFFHAND) return -1; + return result.slot(); + } + + private int swordSlot() { + for (int i = 0; i < 9; i++) { + ItemStack stack = mc.player.getInventory().getItem(i); + if (stack.isEmpty()) continue; + if (stack.is(ItemTags.SWORDS) || stack.is(ItemTags.AXES)) return i; + } + return -1; + } + + /** Tells the server we hold {@code slot} without touching the client's own selection. */ + private static void silentSelect(int slot) { + mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot)); + } + + /** Puts the server back on whatever the client is really holding. */ + private static void silentRestore() { + mc.getConnection().send(new ServerboundSetCarriedItemPacket(InventoryUtil.selected())); + } + + private static double square(double value) { + return value * value; + } + + // ------------------------------------------------------------------ + // Rendering + // ------------------------------------------------------------------ + + @Override + public void onRender3D(Render3DEvent event) { + if (renderPos == null || target == null) return; + if (crystalSlot() == -1 && !offhand) return; + + AABB box = new AABB(renderPos); + RenderUtil.drawBoxFilled(event.getMatrix(), box, fillColor.getValue()); + RenderUtil.drawBox(event.getMatrix(), box, lineColor.getValue(), 1.5f); + } + + @Override + public String getDisplayInfo() { + return target == null || displayDmg <= 0 ? null : String.format(Locale.ROOT, "%.1f", displayDmg); + } + + // ------------------------------------------------------------------ + // Types + // ------------------------------------------------------------------ + + private record BreakResult(double damage, Entity calcCrystal, Angles rots) { + } + + private record PlaceResult(double bestDmg, BlockPos calcPos, Angles rots, boolean isSupport) { + } + + private static final class Result { + final Player target; + final double bestBreakDmg; + final double bestPlaceDmg; + final BlockPos calcPos; + final Entity calcCrystal; + final Angles placeRots; + final Angles breakRots; + final boolean placeIsSupport; + + Result(Player target, BreakResult breakResult, PlaceResult placeResult) { + this.target = target; + this.bestBreakDmg = breakResult.damage; + this.calcCrystal = breakResult.calcCrystal; + this.breakRots = breakResult.rots; + this.bestPlaceDmg = placeResult.bestDmg; + this.calcPos = placeResult.calcPos; + this.placeRots = placeResult.rots; + this.placeIsSupport = placeResult.isSupport(); + } + + double getDamage() { + return Math.max(bestBreakDmg, bestPlaceDmg); + } + } + + public enum Page { + Calc, Place, Break, Misc, Render + } + + public enum Sort { + Damage, Range + } + + public enum AutoSwitch { + None, Normal, Silent + } + + public enum AntiWeakness { + None, Normal, Silent + } + + public enum MultiTask { + None, Soft, Strong + } + + public enum RotationType { + Simple, Silent, GrimAbuse + } + + public enum PlaceMode { + Normal, // Standard placement on obsidian/bedrock + AirPlace, // Place crystals on sides of blocks in air + Support, // Place support blocks (obsidian/end stone) then crystals + AutoSurroundAnchor // Break surround, place anchor, charge+explode + } +} diff --git a/src/main/java/cc/fros/manager/ModuleManager.java b/src/main/java/cc/fros/manager/ModuleManager.java index 5e18d0d..02d9220 100644 --- a/src/main/java/cc/fros/manager/ModuleManager.java +++ b/src/main/java/cc/fros/manager/ModuleManager.java @@ -15,6 +15,7 @@ import cc.fros.features.modules.client.ClickGuiModule; import cc.fros.features.modules.client.HudEditorModule; import cc.fros.features.modules.client.NotificationsModule; import cc.fros.features.modules.combat.KillAuraModule; +import cc.fros.features.modules.combat.CrystalAuraModule; import cc.fros.features.modules.combat.CriticalsModule; import cc.fros.features.modules.combat.KeyPearlModule; import cc.fros.features.modules.hud.ArrayListHudModule; @@ -77,6 +78,7 @@ public class ModuleManager implements Jsonable, Util { register(new NoFallModule()); register(new KeyPearlModule()); register(new KillAuraModule()); + register(new CrystalAuraModule()); register(new FastAccelModule()); LOGGER.info("Registered {} modules", modules.size());