Skip to content

Commit

Permalink
Mob properties to spawn support mobs when a mob has "low" health.
Browse files Browse the repository at this point in the history
 * `support-mobs` loot table or mob type ID of mobs to spawn.
 * `support-percent` percentage chance of spawning mobs
   each time health level conditions are satisfied. If unset
   is effectively 100%.
 * `support-health` how low the mob's health must be for it
   to start summoning mobs. If unset, is effectively max health.
 * `support-health-step` how much the mob's health must decrease
   for new mobs to have a chance of spawning. If unset, any
   health decrease is an opportunity to spawn support mobs when
   the health is below the threshold.
  • Loading branch information
totemo committed Apr 16, 2020
1 parent 4578a5b commit 6108197
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 49 deletions.
140 changes: 94 additions & 46 deletions src/nu/nerd/beastmaster/BeastMaster.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package nu.nerd.beastmaster;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.bukkit.Bukkit;
Expand Down Expand Up @@ -235,6 +237,33 @@ public LivingEntity spawnMob(Location loc, MobType mobType, boolean checkCanFit)
return livingEntity;
}

// ------------------------------------------------------------------------
/**
* Spawn multiple mobs according to a mob property that is either a DropSet
* ID or MobType ID.
*
* @param loc the location to spawn the mob(s).
* @param lootOrMobId the DropSet or MobType ID.
* @param checkCanFit whether to check if the mobs can fit.
* @param results DropResults recording whether vanilla drops happened.
* @param trigger the trigger string to log for logged {@link Drop}s.
* @return a list of the spawned mobs.
*/
public List<LivingEntity> spawnMultipleMobs(Location loc, String lootOrMobId, boolean checkCanFit, DropResults results, String trigger) {
DropSet drops = BeastMaster.LOOTS.getDropSet(lootOrMobId);
if (drops != null) {
drops.generateRandomDrops(results, trigger, null, loc);
return results.getMobs();
} else {
List<LivingEntity> mobs = new ArrayList<>();
MobType supportMobType = BeastMaster.MOBS.getMobType(lootOrMobId);
if (supportMobType != null) {
mobs.add(spawnMob(loc, supportMobType, checkCanFit));
}
return mobs;
}
}

// ------------------------------------------------------------------------
/**
* When a world is loaded, ensure that a top-level zone for that world
Expand Down Expand Up @@ -410,55 +439,37 @@ protected void onProjectileLaunch(ProjectileLaunchEvent event) {
if (shootingMobType == null) {
return;
}
MobProperty projectileMobs = shootingMobType.getDerivedProperty("projectile-mobs");
MobProperty projectileMobsProperty = shootingMobType.getDerivedProperty("projectile-mobs");

// Need to record if projectile removed. isValid() is not true until
// this event returns.
Location projectileLocation = projectile.getLocation();
boolean projectileRemoved = false;
if (projectileMobs.getValue() != null) {
if (projectileMobsProperty.getValue() != null) {
// DropSet or MobType ID:
String id = (String) projectileMobs.getValue();
DropSet drops = BeastMaster.LOOTS.getDropSet(id);
if (drops != null) {
DropResults results = new DropResults();
drops.generateRandomDrops(results, shootingMobType.getId() + " projectile-mobs",
null, projectileLocation);

for (LivingEntity projectileMob : results.getMobs()) {
// Launch the mob with the projectile's velocity.
projectileMob.setVelocity(projectile.getVelocity());

// Target the mob at the shooter's target.
if (target != null && projectileMob instanceof Mob) {
((Mob) projectileMob).setTarget(target);
}
String id = (String) projectileMobsProperty.getValue();
DropResults results = new DropResults();
List<LivingEntity> projectileMobs = spawnMultipleMobs(projectileLocation, id, false, results,
shootingMobType.getId() + " projectile-mobs");
for (LivingEntity mob : projectileMobs) {
// Launch the mob with the projectile's velocity.
mob.setVelocity(projectile.getVelocity());

// Target the mob at the shooter's target.
if (target != null && mob instanceof Mob) {
((Mob) mob).setTarget(target);
}
}

// To have the vanilla drop means not removing the projectile.
// Really requires drop spread to avoid hitting spawned mobs.
if (!results.includesVanillaDrop()) {
event.setCancelled(true);
projectileRemoved = true;
}
} else {
MobType projectileMobType = BeastMaster.MOBS.getMobType(id);
if (projectileMobType != null) {
LivingEntity projectileMob = spawnMob(projectileLocation, projectileMobType, false);
if (projectileMob != null) {
projectileMob.setVelocity(projectile.getVelocity());
if (target != null && projectileMob instanceof Mob) {
((Mob) projectileMob).setTarget(target);
}
event.setCancelled(true);
projectileRemoved = true;
}
}
// To have the vanilla drop means not removing the projectile.
// Really requires drop spread to avoid hitting spawned mobs.
if (!results.includesVanillaDrop()) {
event.setCancelled(true);
projectileRemoved = true;
}
} // if replacing mobs with projectiles
}

// Check that we haven't removed the mob when replacing it in the
// previous step.
// If the projectile was removed, we can't disguise it etc.
if (!projectileRemoved) {
String projectileDisguise = (String) shootingMobType.getDerivedProperty("projectile-disguise").getValue();
BeastMaster.DISGUISES.createDisguise(projectile, projectile.getWorld(), projectileDisguise);
Expand Down Expand Up @@ -507,20 +518,57 @@ protected void onEntityDamage(EntityDamageEvent event) {
return;
}

// If the entity would die, don't play the hurt sound.
// Leave a silence for the death sound.
LivingEntity living = (LivingEntity) entity;
if (event.getFinalDamage() >= living.getHealth()) {
// If the entity would die, don't summon support and don't play the hurt
// sound. Leave a silence for the death sound.
LivingEntity damagedLiving = (LivingEntity) entity;
double finalHealth = damagedLiving.getHealth() - event.getFinalDamage();
if (finalHealth <= 0.0) {
return;
}

MobType mobType = getMobType(entity);
if (mobType != null) {
Location mobLocation = entity.getLocation();

// Support mobs.
String supportId = (String) mobType.getProperty("support-mobs").getValue();
if (supportId != null) {
Double healthThreshold = (Double) mobType.getProperty("support-health").getValue();
boolean healthLow = (healthThreshold == null || finalHealth <= healthThreshold);
Double prevHealth = (Double) EntityMeta.api().get(entity, this, "support-health");
Double healthStep = (Double) mobType.getProperty("support-health-step").getValue();
Double supportPercent = (Double) mobType.getProperty("support-percent").getValue();

if (healthLow && (prevHealth == null ||
healthStep == null ||
prevHealth - finalHealth >= healthStep)
&& (supportPercent == null ||
Math.random() * 100 < supportPercent)) {

// Summon support mobs targeting same target as summoner.
DropResults results = new DropResults();
List<LivingEntity> supportMobs = spawnMultipleMobs(mobLocation, supportId, false, results,
mobType.getId() + " support-mobs");
if (damagedLiving instanceof Mob) {
for (LivingEntity mob : supportMobs) {
if (mob instanceof Mob) {
((Mob) mob).setTarget(((Mob) damagedLiving).getTarget());
}
}
}
// TODO: Spread them out?

// Record the mob health when support mobs were last
// spawned.
EntityMeta.api().set(entity, this, "support-health", finalHealth);
}
}

DamageCause cause = event.getCause();
String propertyName = (cause == DamageCause.PROJECTILE) ? "projectile-hurt-sound" : "melee-hurt-sound";
SoundEffect hurtSound = (SoundEffect) mobType.getDerivedProperty(propertyName).getValue();
if (hurtSound != null) {
Bukkit.getScheduler().runTaskLater(this, () -> hurtSound.play(entity.getLocation()), 1);
Bukkit.getScheduler().runTaskLater(this, () -> hurtSound.play(mobLocation), 1);
}

if (cause == DamageCause.PROJECTILE) {
Expand All @@ -530,7 +578,7 @@ protected void onEntityDamage(EntityDamageEvent event) {
event.setCancelled(true);
SoundEffect immunitySound = (SoundEffect) mobType.getDerivedProperty("projectile-immunity-sound").getValue();
if (immunitySound != null) {
Bukkit.getScheduler().runTaskLater(this, () -> immunitySound.play(entity.getLocation()), 1);
Bukkit.getScheduler().runTaskLater(this, () -> immunitySound.play(mobLocation), 1);
}
return;
}
Expand All @@ -545,7 +593,7 @@ protected void onEntityDamage(EntityDamageEvent event) {
Double hurtTeleportPercent = (Double) mobType.getDerivedProperty("hurt-teleport-percent").getValue();
if (hurtTeleportPercent != null && Math.random() * 100 < hurtTeleportPercent) {
// Find a location up to 10 blocks up and up to 15 blocks away.
Location oldLoc = entity.getLocation();
Location oldLoc = mobLocation;
double range = Util.random(5.0, 15.0);
double angle = Util.random() * 2.0 * Math.PI;
Location newLoc = oldLoc.clone().add(range * Math.cos(angle), 0, range * Math.sin(angle));
Expand Down
13 changes: 10 additions & 3 deletions src/nu/nerd/beastmaster/mobs/MobType.java
Original file line number Diff line number Diff line change
Expand Up @@ -653,11 +653,18 @@ protected void addProperties() {
addProperty(new MobProperty("projectile-removed", DataType.BOOLEAN, null));
addProperty(new MobProperty("projectile-immunity-percent", DataType.DOUBLE, null));

// TODO: projectile-substitution to replace one type of projectile with
// a different type of projectile.

addProperty(new MobProperty("hurt-teleport-percent", DataType.DOUBLE, null));

// Support Mobs -------------------------------------------------------
// support-... properties are implemented in EntityDmanageEvent.
// The mechanism is distinct from vanilla zombie reinforcements.
addProperty(new MobProperty("support-mobs", DataType.LOOT_OR_MOB, null));
addProperty(new MobProperty("support-percent", DataType.DOUBLE, null));
addProperty(new MobProperty("support-health", DataType.DOUBLE, null));
addProperty(new MobProperty("support-health-step", DataType.DOUBLE, null));

// TODO: projectile-substitution to replace one type of projectile with
// a different type of projectile.
// TODO: particle effects tracking mob, projectiles, attack hit points.
}

Expand Down

0 comments on commit 6108197

Please sign in to comment.