package tc.oc.pgm.death; import java.util.NavigableSet; import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; import com.google.common.collect.ImmutableSet; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TranslatableComponent; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.EntityType; import tc.oc.commons.core.chat.Component; import tc.oc.commons.core.chat.Components; import tc.oc.commons.bukkit.chat.NameStyle; import tc.oc.commons.bukkit.localization.Translations; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.ParticipantState; import tc.oc.pgm.tracker.damage.BlockInfo; import tc.oc.pgm.tracker.damage.DamageInfo; import tc.oc.pgm.tracker.damage.EntityInfo; import tc.oc.pgm.tracker.damage.ExplosionInfo; import tc.oc.pgm.tracker.damage.FallInfo; import tc.oc.pgm.tracker.damage.FallingBlockInfo; import tc.oc.pgm.tracker.damage.FireInfo; import tc.oc.pgm.tracker.damage.GenericDamageInfo; import tc.oc.pgm.tracker.damage.ItemInfo; import tc.oc.pgm.tracker.damage.MeleeInfo; import tc.oc.pgm.tracker.damage.MobInfo; import tc.oc.pgm.tracker.damage.PhysicalInfo; import tc.oc.pgm.tracker.damage.PotionInfo; import tc.oc.pgm.tracker.damage.ProjectileInfo; import tc.oc.pgm.tracker.damage.RangedInfo; import tc.oc.pgm.tracker.damage.SpleefInfo; import tc.oc.pgm.tracker.damage.TrackerInfo; public class DeathMessageBuilder { static final Set<String> SPLAT_KEYS = ImmutableSet.of("death.fall.ground", "death.fall.ground.distance"); static final Set<UUID> SPLAT_PLAYERS = ImmutableSet.of( UUID.fromString("6f21f5e3-544b-48a9-9670-63aec36038c4") // HIVE_Raven ); static class NoMessage extends Exception {} static NavigableSet<String> allKeys; static NavigableSet<String> getAllKeys() { if(allKeys == null) { allKeys = Translations.get().getKeys("death."); } return allKeys; } private static final long SNIPE_DISTANCE = 60; private static final int TRIPPED_HEIGHT = 5; private static final int NOTABLE_HEIGHT = 12; private static final int ORBIT_HEIGHT = 60; private final Logger logger; private final MatchPlayer victim; private final @Nullable ParticipantState killer; private String key; private BaseComponent weapon = Components.blank(); private BaseComponent mob = Components.blank(); private Long distance; public DeathMessageBuilder(MatchPlayer victim, DamageInfo damageInfo, Logger logger) { this.victim = victim; this.killer = damageInfo.getAttacker(); this.logger = logger; build(damageInfo); } public BaseComponent getMessage() { return new TranslatableComponent(key, getArgs()); } BaseComponent[] getArgs() { BaseComponent[] args = new BaseComponent[5]; args[0] = victim.getStyledName(NameStyle.COLOR); args[1] = killer == null ? Components.blank() : killer.getStyledName(NameStyle.COLOR); args[2] = weapon; args[3] = mob; args[4] = distance == null ? Components.blank() : new Component(String.valueOf(distance)); return args; } void setDistance(double n) { if(!Double.isNaN(n)) { distance = Math.round(Math.max(0, n)); if(distance == 1l) distance = 2l; // Cleverly ensure the text is always plural } } /* * Primitive methods for manipulating the key */ /** * Test if the given string is a prefix of any existing key */ boolean exists(String prefix) { String key = getAllKeys().ceiling(prefix); return key != null && key.startsWith(prefix); } /** * Return a new key built from the current key with the given tokens appended */ String append(String... tokens) { String newKey = key; for(String token : tokens) { newKey += '.' + token; } return newKey; } /** * Try to append an optional sequence of tokens to the current key. * If the new key is invalid, the current key is not changed. */ boolean option(String... tokens) { String newKey = append(tokens); if(exists(newKey)) { key = newKey; return true; } return false; } /** * Append a sequence of tokens to the current key. * @throws NoMessage if the new key is not valid */ void require(String... tokens) throws NoMessage { String newKey = append(tokens); if(!exists(newKey)) { logger.warning("Generated invalid death message key: " + newKey); throw new NoMessage(); } key = newKey; } /** * Assert that the current key is complete and valid. * @throws NoMessage if it's not */ void finish() throws NoMessage { if(!getAllKeys().contains(key)) { throw new NoMessage(); } } /* * Optional components * * These methods all try * to append something to the key, and return true if successful. * If they fail, they leave the key unchanged. */ boolean variant() { int count = 0; for(; getAllKeys().contains(key + "." + count); count++); if(count == 0) return false; int variant; if(SPLAT_KEYS.contains(key) && count > 1) { // Variant 0 of fall message is reserved for special friends if(SPLAT_PLAYERS.contains(victim.getBukkit().getUniqueId())) { variant = 0; } else { variant = 1 + victim.getMatch().getRandom().nextInt(count - 1); } } else { variant = victim.getMatch().getRandom().nextInt(count); } key += "." + variant; return true; } boolean ranged(RangedInfo rangedInfo, @Nullable Location distanceReference) { double distance = rangedInfo.distanceFrom(distanceReference); if(!Double.isNaN(distance) && option("distance")) { setDistance(distance); if(distance >= SNIPE_DISTANCE) { option("snipe"); } return true; } return false; } boolean potion(PotionInfo potionInfo) { if(option("potion")) { weapon = potionInfo.getLocalizedName(); return true; } return false; } boolean item(ItemInfo itemInfo) { if(itemInfo.getItem().getType() != Material.AIR && option("item")) { weapon = itemInfo.getLocalizedName(); return true; } return false; } boolean block(BlockInfo blockInfo) { if(option("block")) { weapon = blockInfo.getLocalizedName(); return true; } return false; } boolean entity(EntityInfo entityInfo) { if(option("entity")) { weapon = entityInfo.getLocalizedName(); option(entityInfo.getIdentifier()); return true; } return false; } boolean insentient(@Nullable PhysicalInfo info) { if(info instanceof PotionInfo) { if(potion((PotionInfo) info)) { return true; } else if(option("entity")) { // PotionInfo.getLocalizedName returns a potion name, // which doesn't work outside a potion death message. weapon = new TranslatableComponent("item.potion.name"); return true; } } else if(info instanceof EntityInfo) { return !(info instanceof MobInfo) && entity((EntityInfo) info); } else if(info instanceof BlockInfo) { return block((BlockInfo) info); } else if(info instanceof ItemInfo) { return item((ItemInfo) info); } return false; } boolean mob(MobInfo mobInfo) { if(option("mob")) { mob = mobInfo.getLocalizedName(); option(mobInfo.getIdentifier()); return true; } return false; } boolean physical(@Nullable PhysicalInfo info) { if(info instanceof MobInfo) { return mob((MobInfo) info); } else { return insentient(info); } } /* * Required components * * Each of these methods appends several keys to the death message, * and generally expects to complete successfully. If they fail, they * throw a {@link NoMessage} exception and leave the key in an * unknown state. */ void player() throws NoMessage { if(killer != null) { require("player"); } } void attack(@Nullable PhysicalInfo attacker, @Nullable PhysicalInfo weapon) throws NoMessage { player(); if(attacker instanceof MobInfo && !mob((MobInfo) attacker)) { return; } insentient(weapon); } void generic(GenericDamageInfo info) throws NoMessage { switch(info.getDamageType()) { case CONTACT: require("cactus"); break; case DROWNING: require("drown"); break; case LIGHTNING: require("lightning"); break; case STARVATION: require("starve"); break; case SUFFOCATION: require("suffocate"); break; case CUSTOM: require("generic"); break; } // If we don't know the cause, but we already have a full message (e.g. for a fall), // just use what we have. Otherwise, use the "unknown" message. if(exists(key)) return; require("unknown"); } void melee(MeleeInfo melee) throws NoMessage { require("melee"); attack(melee, melee.getWeapon()); } void magic(PotionInfo potion, @Nullable PhysicalInfo attacker) throws NoMessage { require("magic"); attack(attacker, potion); } void projectile(ProjectileInfo projectile, Location distanceReference) throws NoMessage { if(projectile.getProjectile() instanceof PotionInfo) { try { magic((PotionInfo) projectile.getProjectile(), projectile.getShooter()); return; } catch(NoMessage ignored) { // If we can't generate a magic message (probably because it's part // of a fall message), fall back to a projectile message. } } require("projectile"); if(projectile.getProjectile() instanceof EntityInfo && ((EntityInfo) projectile.getProjectile()).getEntityType() == EntityType.ARROW) { // "shot by arrow" is redundant attack(projectile.getShooter(), null); } else { attack(projectile.getShooter(), projectile.getProjectile()); weapon = projectile.getLocalizedName(); // Projectile name may be different than entity name e.g. custom projectile } ranged(projectile, distanceReference); } void squash(FallingBlockInfo fallingBlock) throws NoMessage { require("squash"); attack(null, fallingBlock); } void explosion(ExplosionInfo explosion, Location distanceReference) throws NoMessage { require("explosive"); player(); physical(explosion.getExplosive()); ranged(explosion, distanceReference); } void fire(FireInfo fire) throws NoMessage { require("fire"); player(); if(!(fire.getIgniter() instanceof BlockInfo && ((BlockInfo) fire.getIgniter()).getMaterial().getItemType() == Material.FIRE)) { // "burned by fire" is redundant physical(fire.getIgniter()); } } void fall(FallInfo fall) throws NoMessage { require("fall"); require(fall.getTo().name().toLowerCase()); TrackerInfo cause = fall.getCause(); if(cause instanceof SpleefInfo) { require("spleef"); DamageInfo breaker = ((SpleefInfo) cause).getBreaker(); if(breaker instanceof ExplosionInfo) { explosion((ExplosionInfo) breaker, fall.getOrigin()); } else { player(); } } else if(cause instanceof DamageInfo) { damage((DamageInfo) cause, fall.getOrigin()); } else if(fall.getTo() == FallInfo.To.GROUND) { setDistance(fall.distanceFrom(victim.getBukkit().getLocation())); if(distance != null) { if(distance <= TRIPPED_HEIGHT) { // Very short falls get a "tripped" message option("tripped"); } else if(distance >= ORBIT_HEIGHT) { // Very long falls get an "orbit" message option("orbit"); } else if(victim.getMatch().getRandom().nextFloat() < 0.01f) { // Occasionally they get a rare message option("rare"); } // Show distance if it's high enough and the message supports it if(distance >= NOTABLE_HEIGHT) option("distance"); } } } void damage(DamageInfo info, Location distanceReference) throws NoMessage { if(info instanceof MeleeInfo) { melee((MeleeInfo) info); } else if(info instanceof ProjectileInfo) { projectile((ProjectileInfo) info, distanceReference); } else if(info instanceof ExplosionInfo) { explosion((ExplosionInfo) info, distanceReference); } else if(info instanceof FireInfo) { fire((FireInfo) info); } else if(info instanceof PotionInfo) { magic((PotionInfo) info, null); } else if(info instanceof FallingBlockInfo) { squash((FallingBlockInfo) info); } else if(info instanceof FallInfo) { fall((FallInfo) info); } else if(info instanceof GenericDamageInfo) { generic((GenericDamageInfo) info); } else { throw new NoMessage(); } } void build(DamageInfo damageInfo) { logger.fine("Generating death message for " + damageInfo); try { key = "death"; damage(damageInfo, victim.getBukkit().getLocation()); variant(); finish(); } catch(NoMessage ex) { logger.log(Level.SEVERE, "Generated invalid death message '" + key + "' for victim=" + victim + " info=" + damageInfo + " killer=" + killer + " weapon=" + weapon + " mob=" + mob + " distance=" + distance, ex); key = "death.generic"; } } }