add CrystalAura combat module

Places and breaks end crystals around the nearest non-friend player,
ported from telaviv's CatAura: damage-sorted target calc with min/self
damage and anti-suicide gates, place modes (Normal/AirPlace/Support/
AutoSurroundAnchor), break delays with inhibit and anti-weakness,
silent or normal auto-switch, rotation via the rotation manager, and a
box render of the current placement.

Dropped the dead AntiFeetPlace toggle (never read upstream) and the
unused Sequence setting.
This commit is contained in:
froslanz
2026-10-24 18:31:25 -04:00
parent 453ac20c4c
commit 18095e351d
2 changed files with 931 additions and 0 deletions
@@ -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> page = mode("Page", Page.Calc);
private final Setting<Sort> sort = mode("Sort", Sort.Damage);
private final Setting<Double> targetRange = num("TargetRange", 7.0, 3.0, 20.0);
private final Setting<Double> minDmg = num("MinDamage", 2.0, 0.0, 36.0);
private final Setting<Double> maxSelfDmg = num("MaxSelfDamage", 4.0, 0.0, 36.0);
private final Setting<Boolean> noSuicide = bool("AntiSuicide", false);
private final Setting<Integer> lethalCrystals = num("LethalCrystals", 0, 0, 5);
private final Setting<Boolean> terrain = bool("Terrain", false);
private final Setting<MultiTask> multiTask = mode("MultiTask", MultiTask.None);
// ---- Place ----
private final Setting<PlaceMode> placeMode = mode("PlaceMode", PlaceMode.Normal);
private final Setting<Double> placeRange = num("PlaceRange", 6.0, 1.0, 6.0);
private final Setting<Double> placeWallsRange = num("PlaceWallsRange", 3.0, 1.0, 6.0);
private final Setting<Integer> placeDelay = num("PlaceDelay", 1, 0, 1000);
private final Setting<AutoSwitch> autoSwitch = mode("AutoSwitch", AutoSwitch.Normal);
private final Setting<Boolean> pause = bool("Pause", false);
private final Setting<Boolean> strictDirection = bool("StrictDirection", false);
private final Setting<Boolean> onePointTwelve = bool("1.12", false);
// ---- AutoSurroundAnchor ----
private final Setting<Integer> chargeDelay = num("ChargeDelay", 2, 0, 20);
private final Setting<Integer> explodeDelay = num("ExplodeDelay", 1, 0, 20);
// ---- Break ----
private final Setting<Double> breakRange = num("BreakRange", 6.0, 1.0, 6.0);
private final Setting<Double> breakWallsRange = num("BreakWallsRange", 3.0, 1.0, 6.0);
private final Setting<Integer> breakDelay = num("BreakDelay", 1, 0, 1000);
private final Setting<Integer> ticksExisted = num("TicksExisted", 0, 0, 10);
private final Setting<Boolean> inhibit = bool("Inhibit", false);
private final Setting<AntiWeakness> antiWeakness = mode("AntiWeakness", AntiWeakness.None);
// ---- Misc ----
private final Setting<Boolean> rotate = bool("Rotate", false);
private final Setting<RotationType> rotationType = mode("RotationType", RotationType.Simple);
// ---- Render ----
private final Setting<Color> fillColor = color("FillColor", 0, 0, 0, 100);
private final Setting<Color> lineColor = color("OutlineColor", 255, 255, 255, 255);
private final Timer placeTimer = new Timer();
private final Timer breakTimer = new Timer();
public final Map<Integer, Long> 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<LivingEntity> 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<BlockPos> 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
}
}
@@ -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());