Skip to content

Instantly share code, notes, and snippets.

@Exerosis
Created February 20, 2018 23:11
Show Gist options
  • Save Exerosis/a4e41f6562fdc8777e7fdefe9bd7727a to your computer and use it in GitHub Desktop.
Save Exerosis/a4e41f6562fdc8777e7fdefe9bd7727a to your computer and use it in GitHub Desktop.
package me.exerosis.example;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Zombie;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import static java.lang.Math.max;
import static java.lang.Math.sqrt;
import static java.util.concurrent.ThreadLocalRandom.current;
import static java.util.stream.Stream.concat;
import static java.util.stream.Stream.of;
import static org.bukkit.Bukkit.*;
import static org.bukkit.Material.IRON_SWORD;
import static org.bukkit.Material.NETHER_STAR;
import static org.bukkit.Particle.BLOCK_DUST;
import static org.bukkit.Sound.ENTITY_WITHER_DEATH;
import static org.bukkit.attribute.Attribute.*;
import static org.bukkit.attribute.AttributeModifier.Operation.ADD_SCALAR;
import static org.bukkit.boss.BarColor.RED;
import static org.bukkit.boss.BarStyle.SOLID;
import static org.bukkit.craftbukkit.v1_12_R1.inventory.CraftItemStack.asBukkitCopy;
import static org.bukkit.craftbukkit.v1_12_R1.inventory.CraftItemStack.asNMSCopy;
import static org.bukkit.entity.EntityType.ZOMBIE;
import static org.bukkit.event.EventPriority.MONITOR;
class BossZombie implements Listener {
private final Plugin plugin;
private final Zombie zombie;
private final BossBar bar;
public BossZombie(Plugin plugin, Location location) {
this.plugin = plugin;
getPluginManager().registerEvents(this, plugin);
World world = location.getWorld();
//Ok first thing to do is spawn our boss into the world.
zombie = (Zombie) world.spawnEntity(location, ZOMBIE);
//Next up lets give it a scary name.
zombie.setCustomName(ChatColor.RED + "World Eater >:)");
zombie.setCustomNameVisible(true);
//Then lets make it follow you to the ends of the earth.
scaleAttribute(zombie, GENERIC_FOLLOW_RANGE, "Scale Range", 10);
//We should create a boss bar so players can easily tell how much HP it has.
bar = createBossBar(zombie.getCustomName(), RED, SOLID);
getOnlinePlayers().forEach(bar::addPlayer);
//Now we need to make it a little tougher by modifying it's attributes.
//First lets increase it's health to 4 times a normal zombie
//then make sure he has full health.
AttributeInstance attribute = zombie.getAttribute(GENERIC_MAX_HEALTH);
attribute.addModifier(new AttributeModifier("Quad HP", 4, ADD_SCALAR));
zombie.setHealth(attribute.getValue());
//Next lets increase it's speed by 50%
scaleAttribute(zombie, GENERIC_MOVEMENT_SPEED, "Double Speed", 2);
//And it's DPS by 100%
scaleAttribute(zombie, GENERIC_ATTACK_DAMAGE, "Double Atk Damage", 2);
scaleAttribute(zombie, GENERIC_ATTACK_SPEED, "Double Atk Speed", 2);
//Now mainly for aesthetics lets give it a sword that does no damage.
//It makes the math easier when we want to edit the damage.
net.minecraft.server.v1_12_R1.ItemStack stack = asNMSCopy(new ItemStack(IRON_SWORD));
if (stack.hasTag())
stack.getTag().setInt("generic.attackDamage", 0);
zombie.getEquipment().setItemInMainHand(asBukkitCopy(stack));
}
public class BossBabyZombie {
private final Zombie baby;
public BossBabyZombie() {
//We start by spawning the baby right on top of it's parent.
Location location = zombie.getEyeLocation();
baby = (Zombie) location.getWorld().spawnEntity(location, ZOMBIE);
//Now we need to find it a good landing zone.
int x = location.getBlockY() + current().nextInt(-4, 4);
int z = location.getBlockY() + current().nextInt(-4, 4);
int y = location.getWorld().getHighestBlockYAt(x, z);
//Then we need to calculate a trajectory that will get it there with a nice ark.
baby.setVelocity(calculateVelocity(location.toVector(), new Vector(x, y, z), 2.5));
//Boost em up a little
scaleAttribute(baby, GENERIC_MOVEMENT_SPEED, "Quad Speed", 4);
scaleAttribute(baby, GENERIC_ATTACK_DAMAGE, "Double Atk Damage", 2);
scaleAttribute(baby, GENERIC_FOLLOW_RANGE, "Scale Range", 10);
//Make sure they don't go running off.
baby.setTarget(zombie.getTarget());
//Not entirely sure how this will look, but I figure we should give them a little trail.
new BukkitRunnable() {
@Override
public void run() {
baby.getWorld().spawnParticle(BLOCK_DUST, baby.getLocation(), 4, 1, 1, 1);
if (baby.isOnGround())
cancel();
}
}.runTaskTimer(plugin, 2, 2);
}
}
@EventHandler
void onJoin(PlayerJoinEvent event) {
//Show players the bar as they join.
bar.addPlayer(event.getPlayer());
}
@EventHandler
void onDeath(EntityDeathEvent event) {
if (!event.getEntity().equals(zombie))
return;
//Reward the player for a great kill!
event.setDroppedExp(10_000);
event.getDrops().add(new ItemStack(NETHER_STAR));
//And play a crazy death sound.
zombie.getLocation().getWorld().playSound(zombie.getLocation(), ENTITY_WITHER_DEATH, 1, 1);
//Maybe even some blood particles if u want.
}
@EventHandler(priority = MONITOR, ignoreCancelled = true)
void onDamage(EntityDamageEvent event) {
if (!event.getEntity().equals(zombie))
return;
//Get our zombies health as a percent of the max.
double health = asPercentage(zombie, $ -> zombie.getHealth() - event.getFinalDamage());
//Check if our zombie is at less than 25% health.
if (health < 0.25) {
//Now we kick things up a notch by spitting out a bunch of little baby zombies.
new Runnable() {
List<Zombie> babies = new ArrayList<>();
@Override
public void run() {
if (zombie.isDead())
return;
//Update the boss bar to reflect the health of the babies.
BossBabyZombie baby = new BossBabyZombie();
babies.add(baby.baby);
//Another confusing bit of code
//All it does is get the combine percent health of all the mobs.
double percentHealth = allZombies()
.map(zombie -> zombie.getAttribute(GENERIC_MAX_HEALTH).getValue())
.reduce((a, b) -> a + b)
.orElse(0.0)
/
allZombies()
.map(Zombie::getHealth)
.reduce((a, b) -> a + b)
.orElse(0.0);
bar.setProgress(percentHealth);
//Run this again later, but we want to have somewhat random intervals.
getScheduler().runTaskLater(plugin, this, current().nextInt(2 * 20));
}
private Stream<Zombie> allZombies() {
return concat(of(zombie), babies.stream());
}
}.run();
} else
bar.setProgress(health); //Update the boss bar.
}
//This bit is a little confusing, but that's only because I'm being lazy ;(
private static double asPercentage(LivingEntity entity, Function<LivingEntity, Double> getHealth) {
return getHealth.apply(entity) / entity.getAttribute(GENERIC_MAX_HEALTH).getValue();
}
//We could even make this public and stick it into an EntityUtil
//if we wanted to do this kind of thing a lot.
public static void scaleAttribute(LivingEntity entity, Attribute attribute, String name, double scalar) {
entity.getAttribute(attribute).addModifier(new AttributeModifier(name, scalar, ADD_SCALAR));
}
//Mathy bits.
public static Vector calculateVelocity(Vector from, Vector to, double gain) {
double gravity = 0.667;
Vector subtract = to.subtract(from);
double vertical = subtract.getY();
double horizontal = from.distance(to);
double maxGain = vertical + max(0, gain);
double a = -horizontal * horizontal / (4 * maxGain);
double slope = -horizontal / (2 * a) - sqrt(horizontal * horizontal - 4 * a * -vertical) / (2 * a);
double y = sqrt(maxGain * gravity);
return subtract.multiply(y / slope)
.multiply(1 / Math.sqrt(subtract.setY(0).lengthSquared()))
.setY(y);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment