/**
* Copyright (c) Lambda Innovation, 2013-2016
* This file is part of the AcademyCraft mod.
* https://github.com/LambdaInnovation/AcademyCraft
* Licensed under GPLv3, see project root for more information.
*/
package cn.academy.core.util;
import cn.academy.ability.api.AbilityContext;
import cn.academy.ability.api.AbilityPipeline;
import cn.academy.core.event.BlockDestroyEvent;
import cn.lambdalib.util.generic.MathUtils;
import cn.lambdalib.util.generic.RandUtils;
import cn.lambdalib.util.generic.VecUtils;
import cn.lambdalib.util.helper.Motion3D;
import cn.lambdalib.util.mc.EntitySelectors;
import cn.lambdalib.util.mc.WorldUtils;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.util.ForgeDirection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static cn.lambdalib.util.generic.VecUtils.*;
/**
* A super boomy ranged ray damage. it starts out a ranged ray in the given position and direction,
* and destroy blocks in the path, also damages entities. It takes account of global damage switches.
* @author WeAthFolD
*/
public class RangedRayDamage {
static final double STEP = 0.9;
public final EntityPlayer player;
public final World world;
public final Motion3D motion;
public final AbilityContext ctx;
public double range;
public float totalEnergy; // decrements [hardness of block] when hit a block
public int maxIncrement = 50;
public float dropProb = 0.05f;
public Predicate<Entity> entitySelector = EntitySelectors.everything();
public float startDamage = 10.0f; // ATTN: LINEAR 1.0*startDamage at dist 0; 0.2 * startDamage at maxIncrement
private Vec3 start, slope;
public RangedRayDamage(AbilityContext ctx, double _range, float _energy) {
this.ctx = ctx;
this.motion = new Motion3D(ctx.player, true).move(0.1);
this.player = ctx.player;
this.world = ctx.player.worldObj;
this.range = _range;
this.totalEnergy = _energy;
entitySelector = EntitySelectors.exclude(ctx.player);
}
/**
* BOOM!
*/
public void perform() {
motion.normalize();
Set<int[]> processed = new HashSet<>();
float yaw = -MathUtils.PI_F * 0.5f - motion.getRotationYawRadians(),
pitch = motion.getRotationPitchRadians();
start = motion.getPosVec();
slope = motion.getMotionVec();
Vec3 vp0 = VecUtils.vec(0, 0, 1);
vp0.rotateAroundZ(pitch);
vp0.rotateAroundY(yaw);
Vec3 vp1 = VecUtils.vec(0, 1, 0);
vp1.rotateAroundZ(pitch);
vp1.rotateAroundY(yaw);
double maxDistance = Double.MAX_VALUE;
/* Apply Entity Damage */ {
Vec3 v0 = add(start, add(multiply(vp0, -range), multiply(vp1, -range))),
v1 = add(start, add(multiply(vp0, range), multiply(vp1, -range))),
v2 = add(start, add(multiply(vp0, range), multiply(vp1, range))),
v3 = add(start, add(multiply(vp0, -range), multiply(vp1, range))),
v4 = add(v0, multiply(slope, maxIncrement)),
v5 = add(v1, multiply(slope, maxIncrement)),
v6 = add(v2, multiply(slope, maxIncrement)),
v7 = add(v3, multiply(slope, maxIncrement));
AxisAlignedBB aabb = WorldUtils.minimumBounds(v0, v1, v2, v3, v4, v5, v6, v7);
Predicate<Entity> areaSelector = target -> {
Vec3 dv = subtract(vec(target.posX, target.posY, target.posZ), start);
Vec3 proj = dv.crossProduct(slope);
return proj.lengthVector() < range * 1.2;
};
List<Entity> targets = WorldUtils.getEntities(world, aabb, entitySelector.and(areaSelector));
targets.sort((lhs, rhs) -> {
double dist1 = ctx.player.getDistanceSq(lhs.posX, lhs.posY, lhs.posZ);
double dist2 = ctx.player.getDistanceSq(rhs.posX, rhs.posY, rhs.posZ);
return Double.valueOf(dist1).compareTo(dist2);
});
for(Entity e : targets) {
if (!attackEntity(e)) {
maxDistance = e.getDistanceSqToEntity(ctx.player);
break;
}
}
}
if(ctx.canBreakBlock(world)) {
for(double s = -range; s <= range; s += STEP) {
for(double t = -range; t <= range; t += STEP) {
double rr = range * RandUtils.ranged(0.9, 1.1);
if(s * s + t * t > rr * rr)
continue;
Vec3 pos = VecUtils.add(start,
VecUtils.add(
VecUtils.multiply(vp0, s),
VecUtils.multiply(vp1, t)));
int[] coords = { (int) pos.xCoord, (int) pos.yCoord, (int) pos.zCoord };
if(processed.contains(coords))
continue;
processed.add(coords);
}
}
float ave = totalEnergy / processed.size();
for(int[] coords : processed) {
processLine(coords[0], coords[1], coords[2],
slope, ave * RandUtils.rangef(0.95f, 1.05f), maxDistance);
}
}
}
private void processLine(int x0, int y0, int z0, Vec3 slope, float energy, double maxDistSq) {
Plotter plotter = new Plotter(x0, y0, z0, slope.xCoord, slope.yCoord, slope.zCoord);
int incrs = 0;
for(int i = 0; i <= maxIncrement && energy > 0; ++i) {
++incrs;
int[] coords = plotter.next();
int x = coords[0], y = coords[1], z = coords[2];
int dx = x0 - x, dy = y0 - y, dz = z0 - z;
int dsq = dx*dx + dy*dy + dz*dz;
if (dsq > maxDistSq) break;
boolean snd = incrs < 20;
energy = destroyBlock(energy, x, y, z, snd);
if(RandUtils.ranged(0, 1) < 0.05) {
ForgeDirection dir = ForgeDirection.values()[RandUtils.rangei(0, 6)];
energy = destroyBlock(energy, x + dir.offsetX, y + dir.offsetY, z + dir.offsetZ, snd);
}
}
}
private float destroyBlock(float energy, int x, int y, int z, boolean snd) {
Block block = world.getBlock(x, y, z);
float hardness = block.getBlockHardness(world, x, y, z);
if(hardness < 0)
hardness = 233333;
if(!MinecraftForge.EVENT_BUS.post(new BlockDestroyEvent(player, x, y, z)) && energy >= hardness) {
if(block.getMaterial() != Material.air) {
block.dropBlockAsItemWithChance(world, x, y, z,
world.getBlockMetadata(x, y, z), dropProb, 0);
if(snd && RandUtils.ranged(0, 1) < 0.1) {
world.playSoundEffect(x + 0.5F, y + 0.5F,
z + 0.5F,
block.stepSound.getBreakSound(),
(block.stepSound.getVolume() + 1.0F) / 2.0F,
block.stepSound.getPitch());
}
}
world.setBlockToAir(x, y, z);
return energy - hardness;
}
return 0;
}
protected boolean attackEntity(Entity target) {
Vec3 dv = subtract(vec(target.posX, target.posY, target.posZ), start);
float dist = Math.min(maxIncrement, (float) dv.crossProduct(slope).lengthVector());
float realDmg = this.startDamage * MathUtils.lerpf(1, 0.2f, dist / maxIncrement);
return applyAttack(target, realDmg);
}
protected boolean applyAttack(Entity target, float damage) {
ctx.attack(target, damage);
return true;
}
public static class Reflectible extends RangedRayDamage {
public final Consumer<Entity> callback;
public Reflectible(AbilityContext ctx,
double _range, float _energy, Consumer<Entity> callback) {
super(ctx, _range, _energy);
this.callback = callback;
}
@Override
protected boolean applyAttack(Entity target, float damage) {
boolean[] result = new boolean[] { true }; // for lambda modification
ctx.attackReflect(target, damage, () -> {
callback.accept(target);
result[0] = false;
});
return result[0];
}
}
}