package com.nisovin.magicspells.spells.instant;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import com.nisovin.magicspells.MagicSpells;
import com.nisovin.magicspells.Subspell;
import com.nisovin.magicspells.events.SpellTargetEvent;
import com.nisovin.magicspells.spelleffects.EffectPosition;
import com.nisovin.magicspells.spells.InstantSpell;
import com.nisovin.magicspells.spells.TargetedLocationSpell;
import com.nisovin.magicspells.util.BlockUtils;
import com.nisovin.magicspells.util.BoundingBox;
import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.util.Util;
public class ParticleProjectileSpell extends InstantSpell implements TargetedLocationSpell {
float startYOffset;
float startForwardOffset;
float projectileVelocity;
float projectileVelocityVertOffset;
float projectileVelocityHorizOffset;
float projectileGravity;
float projectileSpread;
boolean powerAffectsVelocity;
int tickInterval;
float ticksPerSecond;
int specialEffectInterval;
int spellInterval;
String particleName;
float particleSpeed;
int particleCount;
float particleXSpread;
float particleYSpread;
float particleZSpread;
int maxDistanceSquared;
int maxDuration;
float hitRadius;
float verticalHitRadius;
int renderDistance;
boolean hugSurface;
float heightFromSurface;
boolean hitPlayers;
boolean hitNonPlayers;
boolean hitSelf;
boolean hitGround;
boolean hitAirAtEnd;
boolean hitAirAfterDuration;
boolean hitAirDuring;
boolean stopOnHitEntity;
boolean stopOnHitGround;
String landSpellName;
Subspell spell;
ParticleProjectileSpell thisSpell;
Random rand = new Random();
public ParticleProjectileSpell(MagicConfig config, String spellName) {
super(config, spellName);
thisSpell = this;
startYOffset = getConfigFloat("start-y-offset", 1F);
startForwardOffset = getConfigFloat("start-forward-offset", 1F);
projectileVelocity = getConfigFloat("projectile-velocity", 10F);
projectileVelocityVertOffset = getConfigFloat("projectile-vert-offset", 0F);
projectileVelocityHorizOffset = getConfigFloat("projectile-horiz-offset", 0F);
projectileGravity = getConfigFloat("projectile-gravity", 0.25F);
projectileSpread = getConfigFloat("projectile-spread", 0F);
powerAffectsVelocity = getConfigBoolean("power-affects-velocity", true);
tickInterval = getConfigInt("tick-interval", 2);
ticksPerSecond = 20F / (float)tickInterval;
specialEffectInterval = getConfigInt("special-effect-interval", 0);
spellInterval = getConfigInt("spell-interval", 20);
particleName = getConfigString("particle-name", "reddust");
particleSpeed = getConfigFloat("particle-speed", 0.3F);
particleCount = getConfigInt("particle-count", 15);
particleXSpread = getConfigFloat("particle-horizontal-spread", 0.3F);
particleYSpread = getConfigFloat("particle-vertical-spread", 0.3F);
particleZSpread = particleXSpread;
particleXSpread = getConfigFloat("particle-red", particleXSpread);
particleYSpread = getConfigFloat("particle-green", particleYSpread);
particleZSpread = getConfigFloat("particle-blue", particleZSpread);
maxDistanceSquared = getConfigInt("max-distance", 15);
maxDistanceSquared *= maxDistanceSquared;
maxDuration = getConfigInt("max-duration", 0) * 1000;
hitRadius = getConfigFloat("hit-radius", 1.5F);
verticalHitRadius = getConfigFloat("vertical-hit-radius", hitRadius);
renderDistance = getConfigInt("render-distance", 32);
hugSurface = getConfigBoolean("hug-surface", false);
if (hugSurface) {
heightFromSurface = getConfigFloat("height-from-surface", .6F);
} else {
heightFromSurface = 0;
}
hitPlayers = getConfigBoolean("hit-players", false);
hitNonPlayers = getConfigBoolean("hit-non-players", true);
hitSelf = getConfigBoolean("hit-self", false);
hitGround = getConfigBoolean("hit-ground", true);
hitAirAtEnd = getConfigBoolean("hit-air-at-end", false);
hitAirAfterDuration = getConfigBoolean("hit-air-after-duration", false);
hitAirDuring = getConfigBoolean("hit-air-during", false);
stopOnHitEntity = getConfigBoolean("stop-on-hit-entity", true);
stopOnHitGround = getConfigBoolean("stop-on-hit-ground", true);
landSpellName = getConfigString("spell", "explode");
}
@Override
public void initialize() {
super.initialize();
Subspell s = new Subspell(landSpellName);
if (s.process()) {
spell = s;
} else {
MagicSpells.error("ParticleProjectileSpell " + internalName + " has an invalid spell defined!");
}
}
@Override
public PostCastAction castSpell(Player player, SpellCastState state, float power, String[] args) {
if (state == SpellCastState.NORMAL) {
new ProjectileTracker(player, player.getLocation(), power);
playSpellEffects(EffectPosition.CASTER, player);
}
return PostCastAction.HANDLE_NORMALLY;
}
class ProjectileTracker implements Runnable {
Player caster;
float power;
long startTime;
Location startLocation;
Location previousLocation;
Location currentLocation;
Vector currentVelocity;
int currentX;
int currentZ;
int taskId;
List<LivingEntity> inRange;
Map<LivingEntity, Long> immune;
int counter = 0;
public ProjectileTracker(Player caster, Location from, float power) {
this.caster = caster;
this.power = power;
this.startTime = System.currentTimeMillis();
this.startLocation = from.clone();
if (startYOffset != 0) {
this.startLocation.setY(this.startLocation.getY() + startYOffset);
}
if (startForwardOffset != 0) {
this.startLocation.add(this.startLocation.getDirection().clone().multiply(startForwardOffset));
}
this.previousLocation = startLocation.clone();
this.currentLocation = startLocation.clone();
this.currentVelocity = from.getDirection();
if (projectileVelocityHorizOffset != 0) {
Util.rotateVector(this.currentVelocity, projectileVelocityHorizOffset);
}
if (projectileVelocityVertOffset != 0) {
this.currentVelocity.add(new Vector(0, projectileVelocityVertOffset, 0)).normalize();
}
if (projectileSpread > 0) {
this.currentVelocity.add(new Vector(rand.nextFloat() * projectileSpread, rand.nextFloat() * projectileSpread, rand.nextFloat() * projectileSpread));
}
if (hugSurface) {
this.currentLocation.setY((int)this.currentLocation.getY() + heightFromSurface);
this.currentVelocity.setY(0).normalize();
}
if (powerAffectsVelocity) {
this.currentVelocity.multiply(power);
}
this.currentVelocity.multiply(projectileVelocity / ticksPerSecond);
this.taskId = MagicSpells.scheduleRepeatingTask(this, 0, tickInterval);
if (hitPlayers || hitNonPlayers) {
this.inRange = currentLocation.getWorld().getLivingEntities();
Iterator<LivingEntity> iter = inRange.iterator();
while (iter.hasNext()) {
LivingEntity e = iter.next();
if (!hitSelf && caster != null && e.equals(caster)) {
iter.remove();
continue;
}
if (!hitPlayers && e instanceof Player) {
iter.remove();
continue;
}
if (!hitNonPlayers && !(e instanceof Player)) {
iter.remove();
continue;
}
}
}
this.immune = new HashMap<LivingEntity, Long>();
}
@Override
public void run() {
if (caster != null && !caster.isValid()) {
stop();
return;
}
// check if duration is up
if (maxDuration > 0 && startTime + maxDuration < System.currentTimeMillis()) {
if (hitAirAfterDuration && spell != null && spell.isTargetedLocationSpell()) {
spell.castAtLocation(caster, currentLocation, power);
playSpellEffects(EffectPosition.TARGET, currentLocation);
}
stop();
return;
}
// move projectile and apply gravity
previousLocation = currentLocation.clone();
currentLocation.add(currentVelocity);
if (hugSurface) {
if (currentLocation.getBlockX() != currentX || currentLocation.getBlockZ() != currentZ) {
Block b = currentLocation.subtract(0, heightFromSurface, 0).getBlock();
if (BlockUtils.isPathable(b)) {
int attempts = 0;
boolean ok = false;
while (attempts++ < 10) {
b = b.getRelative(BlockFace.DOWN);
if (BlockUtils.isPathable(b)) {
currentLocation.add(0, -1, 0);
} else {
ok = true;
break;
}
}
if (!ok) {
stop();
return;
}
} else {
int attempts = 0;
boolean ok = false;
while (attempts++ < 10) {
b = b.getRelative(BlockFace.UP);
currentLocation.add(0, 1, 0);
if (BlockUtils.isPathable(b)) {
ok = true;
break;
}
}
if (!ok) {
stop();
return;
}
}
currentLocation.setY((int)currentLocation.getY() + heightFromSurface);
currentX = currentLocation.getBlockX();
currentZ = currentLocation.getBlockZ();
}
} else if (projectileGravity != 0) {
currentVelocity.setY(currentVelocity.getY() - (projectileGravity / ticksPerSecond));
}
// show particle
MagicSpells.getVolatileCodeHandler().playParticleEffect(currentLocation, particleName, particleXSpread, particleYSpread, particleZSpread, particleSpeed, particleCount, renderDistance, 0F);
// play effects
if (specialEffectInterval > 0 && counter % specialEffectInterval == 0) {
playSpellEffects(EffectPosition.SPECIAL, currentLocation);
}
counter++;
// cast spell mid air
if (hitAirDuring && counter % spellInterval == 0 && spell.isTargetedLocationSpell()) {
spell.castAtLocation(caster, currentLocation.clone(), power);
}
if (stopOnHitGround && !BlockUtils.isPathable(currentLocation.getBlock())) {
if (hitGround && spell != null && spell.isTargetedLocationSpell()) {
Util.setLocationFacingFromVector(previousLocation, currentVelocity);
spell.castAtLocation(caster, previousLocation, power);
playSpellEffects(EffectPosition.TARGET, currentLocation);
}
stop();
} else if (currentLocation.distanceSquared(startLocation) >= maxDistanceSquared) {
if (hitAirAtEnd && spell != null && spell.isTargetedLocationSpell()) {
spell.castAtLocation(caster, currentLocation.clone(), power);
playSpellEffects(EffectPosition.TARGET, currentLocation);
}
stop();
} else if (inRange != null) {
BoundingBox hitBox = new BoundingBox(currentLocation, hitRadius, verticalHitRadius);
for (int i = 0; i < inRange.size(); i++) {
LivingEntity e = inRange.get(i);
if (!e.isDead() && hitBox.contains(e.getLocation().add(0, 0.6, 0))) {
if (spell != null) {
if (spell.isTargetedEntitySpell()) {
ValidTargetChecker checker = spell.getSpell().getValidTargetChecker();
if (checker != null && !checker.isValidTarget(e)) {
inRange.remove(i);
break;
}
LivingEntity target = e;
float thisPower = power;
SpellTargetEvent event = new SpellTargetEvent(thisSpell, caster, target, thisPower);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
inRange.remove(i);
break;
} else {
target = event.getTarget();
thisPower = event.getPower();
}
spell.castAtEntity(caster, target, thisPower);
playSpellEffects(EffectPosition.TARGET, e);
} else if (spell.isTargetedLocationSpell()) {
spell.castAtLocation(caster, currentLocation.clone(), power);
playSpellEffects(EffectPosition.TARGET, currentLocation);
}
}
if (stopOnHitEntity) {
stop();
} else {
inRange.remove(i);
immune.put(e, System.currentTimeMillis());
}
break;
}
}
Iterator<Map.Entry<LivingEntity, Long>> iter = immune.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<LivingEntity, Long> entry = iter.next();
if (entry.getValue().longValue() < System.currentTimeMillis() - 2000) {
iter.remove();
inRange.add(entry.getKey());
}
}
}
}
public void stop() {
playSpellEffects(EffectPosition.DELAYED, currentLocation);
MagicSpells.cancelTask(taskId);
caster = null;
startLocation = null;
previousLocation = null;
currentLocation = null;
currentVelocity = null;
if (inRange != null) {
inRange.clear();
inRange = null;
}
}
}
@Override
public boolean castAtLocation(Player caster, Location target, float power) {
Location loc = target.clone();
loc.setDirection(caster.getLocation().getDirection());
new ProjectileTracker(caster, target, power);
playSpellEffects(EffectPosition.CASTER, caster);
return true;
}
@Override
public boolean castAtLocation(Location target, float power) {
new ProjectileTracker(null, target, power);
playSpellEffects(EffectPosition.CASTER, target);
return true;
}
}