package tc.oc.pgm.mutation.types.targetable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Range; import org.apache.commons.lang.math.Fraction; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.Creeper; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.PigZombie; import org.bukkit.entity.Slime; import org.bukkit.entity.TNTPrimed; import org.bukkit.entity.Zombie; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.ItemStack; import org.bukkit.util.Vector; import tc.oc.commons.core.random.ImmutableWeightedRandomChooser; import tc.oc.commons.core.random.WeightedRandomChooser; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.Repeatable; import tc.oc.pgm.mutation.types.kit.EnchantmentMutation; import tc.oc.pgm.mutation.types.TargetMutation; import tc.oc.pgm.points.PointProviderAttributes; import tc.oc.pgm.points.RandomPointProvider; import tc.oc.pgm.points.RegionPointProvider; import tc.oc.pgm.regions.CuboidRegion; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.WeakHashMap; import static tc.oc.commons.core.random.RandomUtils.nextBoolean; public class ApocalypseMutation extends TargetMutation { final static ImmutableMap AMOUNT_MAP = new ImmutableMap.Builder() .put(3, 25) .put(5, 20) .put(10, 15) .put(20, 5) .put(50, 1) .build(); final static ImmutableMap STACK_MAP = new ImmutableMap.Builder() .put(1, 100) .put(2, 25) .build(); final static ImmutableMap AERIAL_MAP = new ImmutableMap.Builder() .put(EntityType.VEX, 5) .put(EntityType.BLAZE, 1) .build(); final static ImmutableMap GROUND_MAP = new ImmutableMap.Builder() .put(EntityType.SPIDER, 50) .put(EntityType.ZOMBIE, 40) .put(EntityType.CREEPER, 30) .put(EntityType.HUSK, 20) .put(EntityType.CAVE_SPIDER, 10) .put(EntityType.PIG_ZOMBIE, 1) .build(); final static ImmutableMap RANGED_MAP = new ImmutableMap.Builder() .put(EntityType.SKELETON, 50) .put(EntityType.STRAY, 20) .put(EntityType.BLAZE, 20) .put(EntityType.GHAST, 10) .put(EntityType.SHULKER, 5) .put(EntityType.WITCH, 5) .put(EntityType.WITHER_SKELETON, 1) .build(); final static ImmutableMap FLYABLE_MAP = new ImmutableMap.Builder() .putAll(AERIAL_MAP) .put(EntityType.BAT, 10) .build(); final static ImmutableMap PASSENGER_MAP = new ImmutableMap.Builder() .putAll(RANGED_MAP) .put(EntityType.CREEPER, 40) .put(EntityType.PRIMED_TNT, 1) .build(); final static ImmutableMap CUBE_MAP = new ImmutableMap.Builder() .put(EntityType.SLIME, 10) .put(EntityType.MAGMA_CUBE, 1) .build(); final static WeightedRandomChooser AMOUNT = new ImmutableWeightedRandomChooser<>(AMOUNT_MAP); final static WeightedRandomChooser STACK = new ImmutableWeightedRandomChooser<>(STACK_MAP); final static WeightedRandomChooser AERIAL = new ImmutableWeightedRandomChooser<>(AERIAL_MAP); final static WeightedRandomChooser GROUND = new ImmutableWeightedRandomChooser<>(GROUND_MAP); final static WeightedRandomChooser RANGED = new ImmutableWeightedRandomChooser<>(RANGED_MAP); final static WeightedRandomChooser FLYABLE = new ImmutableWeightedRandomChooser<>(FLYABLE_MAP); final static WeightedRandomChooser PASSENGER = new ImmutableWeightedRandomChooser<>(PASSENGER_MAP); final static WeightedRandomChooser CUBE = new ImmutableWeightedRandomChooser<>(CUBE_MAP); final static int DISTANCE = 15; // Max distance entities spawn from players final static int PARTICIPANT_ENTITIES = 25; // Max entities on the field per participant final static int MAX_ENTITIES = 500; // Max total entities on the field final static Range AIR_OFFSET = Range.closed(DISTANCE / 4, DISTANCE); // Y-axis offset for spawning flying entities final static Fraction SPECIAL_CHANCE = Fraction.ONE_FIFTH; // Chance of a special attribute occuring in an entity final static int SPECIAL_MULTIPLIER = 3; // Multiplier for special attributes final WeakHashMap entities; long time; public ApocalypseMutation(Match match) { super(match, Duration.ofSeconds(20)); this.entities = new WeakHashMap<>(); } /** * Get the maximum amount of entities that can be spawned. */ public int entitiesMax() { return Math.min((int) match.participants().count() * PARTICIPANT_ENTITIES, MAX_ENTITIES); } /** * Get the number of available slots are left for additional entities to spawn. */ public int entitiesLeft() { return entitiesMax() - world.getLivingEntities().size() + (int) match.participants().count(); } /** * Generate a random spawn point given two locations. */ public Optional location(Location start, Location end) { return Optional.ofNullable(new RandomPointProvider(Collections.singleton(new RegionPointProvider(new CuboidRegion(start.position(), end.position()), new PointProviderAttributes()))).getPoint(match, null)); } /** * Spawn a cohort of entities at the given location. * @param location location to spawn the entity. * @param ground whether the location is on the ground. */ public void spawn(Location location, boolean ground) { int slots = entitiesLeft(); int queued = AMOUNT.choose(entropy); // Remove any entities that may be over the max limit despawn(queued - slots); // Determine whether the entities should be airborn int stack = STACK.choose(entropy); boolean air = !ground || nextBoolean(random, SPECIAL_CHANCE); if(air) { stack += (stack == 1 && random.nextBoolean() ? 1 : 0); location.add(0, entropy.randomInt(AIR_OFFSET), 0); } // Select the random entity chooser based on ground, air, and stacked boolean stacked = stack > 1; WeightedRandomChooser chooser; if(air) { if(stacked) { if(ground) { chooser = nextBoolean(random, SPECIAL_CHANCE) ? CUBE : FLYABLE; } else { chooser = FLYABLE; } } else { chooser = AERIAL; } } else { if(stacked) { chooser = GROUND; } else { chooser = random.nextBoolean() ? GROUND : RANGED; } } // Select the specific entity types for the spawn, // all entities will have the same sequence of entity type // but may have variations (like armor) between them. List types = new ArrayList<>(); for(int i = 0; i < stack; i++) { types.add((i == 0 ? chooser : PASSENGER).choose(entropy)); } // Spawn the mobs and stack them if required for(int i = 0; i < queued; i++) { Entity last = null; for(EntityType type : types) { Entity entity = spawn(location, type); if(last != null) { last.setPassenger(entity); } last = entity; } } } /** * Spawn an individual entitiy at a location given an entity type. * @param location the location to spawn at. * @param type the type of entity. * @return the constructed entity. */ public LivingEntity spawn(Location location, EntityType type) { EnchantmentMutation enchant = new EnchantmentMutation(match); LivingEntity entity = (LivingEntity) spawn(location, type.getEntityClass()); EntityEquipment equipment = entity.getEquipment(); entity.setVelocity(Vector.getRandom()); entity.setAbsorption(5); ItemStack held = null; switch(type) { case SKELETON: case WITHER_SKELETON: case STRAY: held = item(Material.BOW); break; case ZOMBIE: case ZOMBIE_VILLAGER: case HUSK: Zombie zombie = (Zombie) entity; zombie.setBaby(nextBoolean(random, SPECIAL_CHANCE)); break; case PIG_ZOMBIE: PigZombie pigZombie = (PigZombie) entity; pigZombie.setAngry(true); pigZombie.setAnger(Integer.MAX_VALUE); held = item(Material.GOLD_SWORD); break; case CREEPER: Creeper creeper = (Creeper) entity; creeper.setPowered(nextBoolean(random, SPECIAL_CHANCE)); world.strikeLightningEffect(location); break; case PRIMED_TNT: TNTPrimed tnt = (TNTPrimed) entity; tnt.setFuseTicks(tnt.getFuseTicks() * SPECIAL_MULTIPLIER); break; case SLIME: case MAGMA_CUBE: Slime slime = (Slime) entity; slime.setSize(slime.getSize() * SPECIAL_MULTIPLIER); break; case SKELETON_HORSE: world.strikeLightning(location); break; } if(held != null && random.nextBoolean()) { enchant.apply(held, equipment); equipment.setItemInMainHand(held); } entities.put(entity, Instant.now()); return entity; } /** * Select the entities that have lived the longest and remove them * to make room for new entities. * @param amount the amount of entities to despawn. */ public void despawn(int amount) { entities.entrySet() .stream() .sorted(Map.Entry.comparingByValue(Comparator.comparing(Instant::toEpochMilli))) .limit(Math.max(0, amount)) .map(Map.Entry::getKey) .forEach(Entity::remove); } @Override public void execute(List players) { // At least one player is required to spawn mobs if(players.size() >= 1) { Location start, end; start = players.get(0).getLocation(); // player 1 is the first location if(players.size() >= 2) { end = players.get(1).getLocation(); // if player 2 exists, they are the second location } else { // if no player 2, generate a random location near player 1 end = start.clone().add(Vector.getRandom().multiply(DISTANCE)); } Optional location = location(start, end); if(location.isPresent()) { // if the location is safe (on ground) spawn(location.get(), true); } else { // if the location was not safe, generate a simple midpoint location spawn(start.position().midpoint(end.position()).toLocation(world), false); } } } @Override public int targets() { return 2; // Always require 2 targets to generate a spawn location between them } @Override public void enable() { super.enable(); time = world.getTime(); } @Repeatable public void tick() { world.setTime(16000); // Night time to prevent flaming entities } @Override public void disable() { world.setTime(time); despawn(entities.size()); super.disable(); } }