port crystal damage and placement helpers from telaviv

CrystalUtil computes explosion exposure/damage with armor, enchants and
resistance reductions; BlockUtil covers placement sides, strict
directions and sight traces; TargetUtil picks the nearest non-friend
player in range.
This commit is contained in:
froslanz
2026-10-24 18:28:05 -04:00
parent 5e2a86d0cd
commit 453ac20c4c
3 changed files with 412 additions and 0 deletions
@@ -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<BlockPos> sphere(double range, BlockPos pos, boolean sphere, boolean hollow) {
List<BlockPos> 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<Direction> getStrictDirections(Vec3 eyePos, Vec3 blockPos) {
List<Direction> 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<BlockPos> exceptions, boolean strictDirection) {
List<Direction> 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;
}
}
@@ -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<ExposureRaycastContext, BlockPos, BlockHitResult> {
}
}
@@ -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<LivingEntity> getTargets(double range) {
List<LivingEntity> 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);
}
}