diff --git a/src/main/java/cc/fros/util/world/BlockUtil.java b/src/main/java/cc/fros/util/world/BlockUtil.java new file mode 100644 index 0000000..f1652c2 --- /dev/null +++ b/src/main/java/cc/fros/util/world/BlockUtil.java @@ -0,0 +1,166 @@ +package cc.fros.util.world; + +import cc.fros.util.traits.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Pose; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Block placement / raytrace helpers, ported from telaviv's {@code BlockUtil}. + */ +public final class BlockUtil implements Util { + private BlockUtil() { + throw new AssertionError(); + } + + public static List sphere(double range, BlockPos pos, boolean sphere, boolean hollow) { + List blocks = new ArrayList<>(); + int cx = pos.getX(); + int cy = pos.getY(); + int cz = pos.getZ(); + + for (int x = cx - (int) range; x <= cx + (int) range; x++) { + for (int z = cz - (int) range; z <= cz + (int) range; z++) { + for (int y = (sphere ? cy - (int) range : cy); y < (cy + range); y++) { + double dist = (cx - x) * (cx - x) + (cz - z) * (cz - z) + (sphere ? (cy - y) * (cy - y) : 0); + if (dist < range * range && !(hollow && dist < (range - 1) * (range - 1))) { + blocks.add(new BlockPos(x, y, z)); + } + } + } + } + return blocks; + } + + public static Direction getPlaceableSideCrystal(BlockPos pos, boolean strict) { + return getPlaceableSideCrystal(pos, strict, null); + } + + public static Direction getPlaceableSideCrystal(BlockPos pos, boolean strict, BlockPos ignore) { + if (mc.player.blockPosition().above().equals(pos) && mc.player.getPose() != Pose.SWIMMING) { + return Direction.UP; + } + + double bestDist = 99999; + Direction bestSide = null; + for (Direction side : Direction.values()) { + BlockPos neighbour = pos.relative(side); + BlockState blockState = mc.level.getBlockState(neighbour); + + if (strict) { + if (!canSeeFace(pos, side)) continue; + if (!blockState.canBeReplaced() && !neighbour.equals(ignore)) continue; + } + + if (side == Direction.UP) return side; + + double dist = distanceTo(pos, side); + if (bestSide == null || dist < bestDist) { + bestDist = dist; + bestSide = side; + } + } + return bestSide; + } + + public static boolean placeTrace(BlockPos pos) { + for (Direction facing : Direction.values()) { + Vec3 vec = new Vec3( + pos.getX() + 0.5 + facing.getStepX() / 2.0, + pos.getY() + 0.5 + facing.getStepY() / 2.0, + pos.getZ() + 0.5 + facing.getStepZ() / 2.0); + + BlockHitResult result = mc.level.clip(new ClipContext( + mc.player.getEyePosition(), vec, + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, mc.player)); + + if (result.getType() != HitResult.Type.MISS && result.getBlockPos().equals(pos)) return false; + } + return true; + } + + public static boolean canSeeFace(BlockPos target, Direction face) { + Vec3 eyes = mc.player.getEyePosition(); + double cx = target.getX() + 0.5; + double cy = target.getY() + 0.5; + double cz = target.getZ() + 0.5; + double px = cx + face.getStepX() * 0.5; + double py = cy + face.getStepY() * 0.5; + double pz = cz + face.getStepZ() * 0.5; + + return switch (face) { + case NORTH -> eyes.z < pz; + case EAST -> eyes.x > px; + case SOUTH -> eyes.z > pz; + case WEST -> eyes.x < px; + case UP -> eyes.y + 0.5 > py; + case DOWN -> eyes.y < py; + }; + } + + public static double distanceTo(BlockPos pos, Direction direction) { + return mc.player.getEyePosition().distanceTo( + new Vec3(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5) + .add(direction.getStepX(), direction.getStepY(), direction.getStepZ())); + } + + public static List getStrictDirections(Vec3 eyePos, Vec3 blockPos) { + List directions = new ArrayList<>(); + + double differenceX = eyePos.x - blockPos.x; + double differenceY = eyePos.y - blockPos.y; + double differenceZ = eyePos.z - blockPos.z; + + if (differenceY > 0.5) directions.add(Direction.UP); + else if (differenceY < -0.5) directions.add(Direction.DOWN); + else { + directions.add(Direction.UP); + directions.add(Direction.DOWN); + } + + if (differenceX > 0.5) directions.add(Direction.EAST); + else if (differenceX < -0.5) directions.add(Direction.WEST); + else { + directions.add(Direction.EAST); + directions.add(Direction.WEST); + } + + if (differenceZ > 0.5) directions.add(Direction.SOUTH); + else if (differenceZ < -0.5) directions.add(Direction.NORTH); + else { + directions.add(Direction.SOUTH); + directions.add(Direction.NORTH); + } + + return directions; + } + + public static Direction getDirection(BlockPos position, boolean strictDirection) { + return getDirection(position, null, strictDirection); + } + + public static Direction getDirection(BlockPos position, List exceptions, boolean strictDirection) { + List strictDirections = strictDirection + ? getStrictDirections(mc.player.getEyePosition(), Vec3.atCenterOf(position)) + : List.of(); + + for (Direction direction : Direction.values()) { + BlockPos offset = position.relative(direction); + if ((!strictDirection || strictDirections.contains(direction.getOpposite())) + && (!mc.level.getBlockState(offset).canBeReplaced() + || (exceptions != null && exceptions.contains(offset)))) { + return direction; + } + } + + return null; + } +} diff --git a/src/main/java/cc/fros/util/world/CrystalUtil.java b/src/main/java/cc/fros/util/world/CrystalUtil.java new file mode 100644 index 0000000..366fba6 --- /dev/null +++ b/src/main/java/cc/fros/util/world/CrystalUtil.java @@ -0,0 +1,198 @@ +package cc.fros.util.world; + +import cc.fros.util.EnchantmentUtil; +import cc.fros.util.traits.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.world.damagesource.CombatRules; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.boss.enderdragon.EndCrystal; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.Difficulty; +import net.minecraft.world.level.block.Blocks; +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.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; + +import java.util.function.BiFunction; + +/** + * Crystal damage / exposure calculation, ported from telaviv's {@code CrystalUtil}. + */ +public final class CrystalUtil implements Util { + private CrystalUtil() { + throw new AssertionError(); + } + + public static boolean canPlaceCrystal(BlockPos pos, boolean protocol) { + BlockState state = mc.level.getBlockState(pos); + + if (state.getBlock() != Blocks.OBSIDIAN && state.getBlock() != Blocks.BEDROCK) return false; + + BlockState crystal = mc.level.getBlockState(pos.above()); + if (!crystal.isAir()) return false; + + if (!protocol) return true; + + return mc.level.getBlockState(pos.above(2)).isAir(); + } + + public static boolean canPlaceCrystalServer(BlockPos block) { + BlockState blockState = mc.level.getBlockState(block); + if (blockState.getBlock() != Blocks.OBSIDIAN && blockState.getBlock() != Blocks.BEDROCK) return false; + + BlockPos up = block.above(); + if (!mc.level.getBlockState(up).isAir()) return false; + + AABB box = new AABB(up.getX(), up.getY(), up.getZ(), up.getX() + 1.0, up.getY() + 2.0, up.getZ() + 1.0); + return mc.level.getEntities(null, box).isEmpty(); + } + + public static boolean canPlaceCrystalAir(BlockPos block) { + BlockPos up = block.above(); + if (!mc.level.getBlockState(up).isAir()) return false; + + AABB box = new AABB(up.getX(), up.getY(), up.getZ(), up.getX() + 1.0, up.getY() + 2.0, up.getZ() + 1.0); + return mc.level.getEntities(null, box).isEmpty(); + } + + public static float calculateDamage(LivingEntity entity, Vec3 crystal, boolean ignoreTerrain) { + return calculateDamage(entity, crystal, ignoreTerrain, null); + } + + public static float calculateDamage(LivingEntity entity, Vec3 crystal, boolean ignoreTerrain, BlockPos ignore) { + double exposure = getExposure(crystal, entity.getBoundingBox(), ignoreTerrain, ignore); + double distanceFactor = Math.sqrt(entity.distanceToSqr(crystal)) / 12.0; + double factor = (1.0 - distanceFactor) * exposure; + float damage = (float) ((int) ((factor * factor + factor) / 2.0 * 7.0 * 12.0 + 1.0)); + damage = calculateReductions(damage, entity, mc.level.damageSources().explosion(null, null)); + return (float) Math.max(0.0, damage); + } + + public static float calculateReductions(float damage, LivingEntity entity, DamageSource source) { + if (source.scalesWithDifficulty()) { + Difficulty difficulty = mc.level.getDifficulty(); + if (difficulty == Difficulty.EASY) damage = Math.min(damage / 2.0f + 1.0f, damage); + else if (difficulty == Difficulty.HARD) damage *= 1.5f; + } + + damage = CombatRules.getDamageAfterAbsorb( + entity, damage, source, + entity.getArmorValue(), + (float) entity.getAttributeValue(Attributes.ARMOR_TOUGHNESS) + ); + + damage = resistanceReduction(entity, damage); + + float protAmount = getProtectionAmount(entity); + if (protAmount > 0) damage = CombatRules.getDamageAfterMagicAbsorb(damage, protAmount); + + return Math.max(damage, 0.0f); + } + + public static int getProtectionAmount(LivingEntity entity) { + int total = 0; + for (EquipmentSlot slot : new EquipmentSlot[]{EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET}) { + total += getProtectionAmount(entity.getItemBySlot(slot)); + } + return total; + } + + public static int getProtectionAmount(ItemStack stack) { + if (stack.isEmpty()) return 0; + int blast = EnchantmentUtil.getLevel(Enchantments.BLAST_PROTECTION, stack); + int prot = EnchantmentUtil.getLevel(Enchantments.PROTECTION, stack); + return blast * 2 + prot; + } + + private static float resistanceReduction(LivingEntity entity, float damage) { + MobEffectInstance resistance = entity.getEffect(MobEffects.RESISTANCE); + if (resistance != null) { + int level = resistance.getAmplifier() + 1; + damage *= (1.0f - level * 0.2f); + } + return Math.max(damage, 0.0f); + } + + private static float getExposure(Vec3 source, AABB box, boolean ignoreTerrain, BlockPos ignorePos) { + RaycastFactory factory = getRaycastFactory(ignoreTerrain, ignorePos); + + double xDiff = box.maxX - box.minX; + double yDiff = box.maxY - box.minY; + double zDiff = box.maxZ - box.minZ; + + double xStep = 1.0 / (xDiff * 2.0 + 1.0); + double yStep = 1.0 / (yDiff * 2.0 + 1.0); + double zStep = 1.0 / (zDiff * 2.0 + 1.0); + + if (xStep > 0 && yStep > 0 && zStep > 0) { + int misses = 0; + int hits = 0; + + double xOffset = (1.0 - Math.floor(1.0 / xStep) * xStep) * 0.5; + double zOffset = (1.0 - Math.floor(1.0 / zStep) * zStep) * 0.5; + + xStep *= xDiff; + yStep *= yDiff; + zStep *= zDiff; + + double startX = box.minX + xOffset; + double startY = box.minY; + double startZ = box.minZ + zOffset; + double endX = box.maxX + xOffset; + double endY = box.maxY; + double endZ = box.maxZ + zOffset; + + for (double x = startX; x <= endX; x += xStep) { + for (double y = startY; y <= endY; y += yStep) { + for (double z = startZ; z <= endZ; z += zStep) { + Vec3 position = new Vec3(x, y, z); + if (raycast(new ExposureRaycastContext(position, source), factory) == null) misses++; + hits++; + } + } + } + + return (float) misses / hits; + } + + return 0.0f; + } + + private static RaycastFactory getRaycastFactory(boolean ignoreTerrain, BlockPos ignorePos) { + return (context, blockPos) -> { + BlockState blockState = mc.level.getBlockState(blockPos); + + if (ignorePos != null && blockPos.equals(ignorePos)) return null; + + if (ignoreTerrain && blockState.getBlock().getExplosionResistance() < 600.0f) return null; + + VoxelShape shape = blockState.getCollisionShape(mc.level, blockPos); + return shape.clip(context.start(), context.end(), blockPos); + }; + } + + private static BlockHitResult raycast(ExposureRaycastContext context, RaycastFactory factory) { + return BlockGetter.traverseBlocks(context.start(), context.end(), context, factory, ctx -> null); + } + + public static boolean isEndCrystal(Entity entity) { + return entity instanceof EndCrystal; + } + + public record ExposureRaycastContext(Vec3 start, Vec3 end) { + } + + @FunctionalInterface + public interface RaycastFactory extends BiFunction { + } +} diff --git a/src/main/java/cc/fros/util/world/TargetUtil.java b/src/main/java/cc/fros/util/world/TargetUtil.java new file mode 100644 index 0000000..5f13826 --- /dev/null +++ b/src/main/java/cc/fros/util/world/TargetUtil.java @@ -0,0 +1,48 @@ +package cc.fros.util.world; + +import cc.fros.Fros; +import cc.fros.util.traits.Util; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple target selection helpers, ported from telaviv's {@code TargetUtil}. + */ +public final class TargetUtil implements Util { + private TargetUtil() { + throw new AssertionError(); + } + + public static LivingEntity getTarget(double range) { + LivingEntity best = null; + double bestDistance = Double.MAX_VALUE; + for (LivingEntity entity : getTargets(range)) { + double distance = mc.player.position().distanceTo(entity.position()); + if (distance < bestDistance) { + bestDistance = distance; + best = entity; + } + } + return best; + } + + public static List getTargets(double range) { + List targets = new ArrayList<>(); + for (Player entity : mc.level.players()) { + if (!isValidTarget(entity)) continue; + if (mc.player.position().distanceTo(entity.position()) > range) continue; + targets.add(entity); + } + return targets; + } + + public static boolean isValidTarget(Player entity) { + if (entity == mc.player) return false; + if (!entity.isAlive() || entity.isRemoved()) return false; + if (entity.isSpectator()) return false; + return !Fros.friendManager.isFriend(entity); + } +}