diff --git a/src/main/java/com/garbagemule/MobArena/MASpawnThread.java b/src/main/java/com/garbagemule/MobArena/MASpawnThread.java index ce719869..a19d1f83 100644 --- a/src/main/java/com/garbagemule/MobArena/MASpawnThread.java +++ b/src/main/java/com/garbagemule/MobArena/MASpawnThread.java @@ -9,12 +9,10 @@ import com.garbagemule.MobArena.things.ExperienceThing; import com.garbagemule.MobArena.things.Thing; import com.garbagemule.MobArena.things.ThingPicker; -import com.garbagemule.MobArena.waves.MABoss; -import com.garbagemule.MobArena.waves.MACreature; -import com.garbagemule.MobArena.waves.Wave; -import com.garbagemule.MobArena.waves.WaveManager; +import com.garbagemule.MobArena.waves.*; import com.garbagemule.MobArena.waves.enums.WaveType; import com.garbagemule.MobArena.waves.types.BossWave; +import com.garbagemule.MobArena.waves.types.ComboWave; import com.garbagemule.MobArena.waves.types.SupplyWave; import com.garbagemule.MobArena.waves.types.UpgradeWave; import org.bukkit.Bukkit; @@ -180,8 +178,32 @@ private void spawnNextWave() { private void spawnWave(int wave) { Wave w = waveManager.next(); + handleWave(w, wave, true); - w.announce(arena, wave); + Wave sw = waveManager.getCurrentShadow(); + if (sw != null && WaveUtils.waveBlocked(sw, w.getShadowBlacklist())) { + handleWave(sw, wave, false); + } + } + + private void handleWave(Wave w, int wave, boolean announce) { + if (w.getType() == WaveType.COMBO) { + ComboWave cw = (ComboWave) w; + + Wave waveA = cw.getWaveA(); + handleWave(cw.getWaveA(), wave, waveA.getType() == WaveType.COMBO); + handleWave(cw.getWaveB(), wave, false); + + + if (announce && waveA.getType() != WaveType.COMBO) { + cw.getWaveA().announce(arena, wave); // announce only waveA in outermost COMBO wave. + } + return; + } + + if (announce) { + w.announce(arena, wave); + } arena.getScoreboard().updateWave(wave); diff --git a/src/main/java/com/garbagemule/MobArena/waves/AbstractWave.java b/src/main/java/com/garbagemule/MobArena/waves/AbstractWave.java index 26079a6d..9fc2b70b 100644 --- a/src/main/java/com/garbagemule/MobArena/waves/AbstractWave.java +++ b/src/main/java/com/garbagemule/MobArena/waves/AbstractWave.java @@ -20,6 +20,7 @@ public abstract class AbstractWave implements Wave private double healthMultiplier, amountMultiplier; private int firstWave, frequency, priority; + private String shadowBlacklist; private List spawnpoints; private List effects; @@ -120,6 +121,16 @@ public void setPriority(int priority) { this.priority = priority; } + @Override + public String getShadowBlacklist() { + return shadowBlacklist; + } + + @Override + public void setShadowBlacklist(String shadowBlacklist) { + this.shadowBlacklist = shadowBlacklist; + } + @Override public double getHealthMultiplier() { return healthMultiplier; diff --git a/src/main/java/com/garbagemule/MobArena/waves/Wave.java b/src/main/java/com/garbagemule/MobArena/waves/Wave.java index 94dce16e..b86ba3df 100644 --- a/src/main/java/com/garbagemule/MobArena/waves/Wave.java +++ b/src/main/java/com/garbagemule/MobArena/waves/Wave.java @@ -160,6 +160,18 @@ public interface Wave */ void setAmountMultiplier(double amountMultiplier); + /** + * Get the wave's shadow blacklist. + * @return a list of shadow wave names + */ + String getShadowBlacklist(); + + /** + * Set the wave's shadow blacklist. + * @param blacklist a list of shadow wave names + */ + void setShadowBlacklist(String blacklist); + /** * Check if this wave matches the wave number. * The SingleWave class does a simple check if its wave == the parameter. diff --git a/src/main/java/com/garbagemule/MobArena/waves/WaveManager.java b/src/main/java/com/garbagemule/MobArena/waves/WaveManager.java index b1dc2e4f..6f29076c 100644 --- a/src/main/java/com/garbagemule/MobArena/waves/WaveManager.java +++ b/src/main/java/com/garbagemule/MobArena/waves/WaveManager.java @@ -15,7 +15,9 @@ public class WaveManager private ConfigurationSection section; private Wave defaultWave, currentWave, nextWave; + private Wave currentShadowWave, nextShadowWave; private TreeSet recurrentWaves, singleWaves; + private TreeSet recurrentShadowWaves, singleShadowWaves; private int wave, finalWave; private Random random = new Random(); @@ -33,6 +35,7 @@ public void reset() { reloadWaves(); wave = 0; determineNextWave(); + determineNextShadowWave(); } public void reloadWaves() { @@ -42,6 +45,12 @@ public void reloadWaves() { recurrentWaves = WaveParser.parseWaves(arena, rConfig, WaveBranch.RECURRENT); singleWaves = WaveParser.parseWaves(arena, sConfig, WaveBranch.SINGLE); + ConfigurationSection shadowRConfig = section.getConfigurationSection("shadow.recurrent"); + ConfigurationSection shadowSConfig = section.getConfigurationSection("shadow.single"); + + recurrentShadowWaves = WaveParser.parseWaves(arena, shadowRConfig, WaveBranch.RECURRENT); + singleShadowWaves = WaveParser.parseWaves(arena, shadowSConfig, WaveBranch.SINGLE); + // getParent() => go back to the arena-node to access settings finalWave = section.getParent().getInt("settings.final-wave", 0); @@ -62,13 +71,13 @@ public void reloadWaves() { private void determineNextWave() { // Single waves take precedence over recurrent waves - List singles = findSingleWaveCandidates(wave + 1); + List singles = findSingleWaveCandidates(wave + 1, singleWaves); if (!singles.isEmpty()) { nextWave = pickRandomWave(singles); return; } - List recurrents = findRecurrentWaveCandidates(wave + 1); + List recurrents = findRecurrentWaveCandidates(wave + 1, recurrentWaves); if (!recurrents.isEmpty()) { nextWave = pickRandomWave(recurrents); } else { @@ -76,9 +85,23 @@ private void determineNextWave() { } } - private List findSingleWaveCandidates(int wave) { - List candidates = new ArrayList<>(singleWaves.size()); - for (Wave w : singleWaves) { + private void determineNextShadowWave() { + // Single waves take precedence over recurrent waves + List singles = findSingleWaveCandidates(wave + 1, singleShadowWaves); + if (!singles.isEmpty()) { + nextShadowWave = pickRandomWave(singles); + return; + } + + List recurrents = findRecurrentWaveCandidates(wave + 1, recurrentShadowWaves); + if (!recurrents.isEmpty()) { + nextShadowWave = pickRandomWave(recurrents); + } + } + + private List findSingleWaveCandidates(int wave, TreeSet waves) { + List candidates = new ArrayList<>(waves.size()); + for (Wave w : waves) { if (w.matches(wave)) { candidates.add(w); } @@ -87,9 +110,9 @@ private List findSingleWaveCandidates(int wave) { return candidates; } - private List findRecurrentWaveCandidates(int wave) { - List matches = new ArrayList<>(recurrentWaves.size()); - for (Wave w : recurrentWaves) { + private List findRecurrentWaveCandidates(int wave, TreeSet waves) { + List matches = new ArrayList<>(waves.size()); + for (Wave w : waves) { if (w.matches(wave)) { matches.add(w); } @@ -123,14 +146,17 @@ private Wave pickRandomWave(List candidates) { /** * Increment the wave number and get the next Wave to be spawned. + * Does NOT return the next Shadow Wave. * Note that this method is a mutator. * @return the next Wave */ public Wave next() { currentWave = nextWave; + currentShadowWave = nextShadowWave; wave++; determineNextWave(); + determineNextShadowWave(); return currentWave; } @@ -154,6 +180,25 @@ public Wave getCurrent() { return currentWave; } + /** + * Get the next Shadow Wave to be spawned. This is an accessor and does not + * advance the "counter". Note that the Wave objects, however, are + * mutable. + * @return the next Shadow Wave + */ + public Wave getNextShadow() { + return nextShadowWave; + } + + /** + * Get the current Shadow wave that's being used. + * Note that the current wave might not have spawned yet. + * @return the current Shadow wave + */ + public Wave getCurrentShadow() { + return currentShadowWave; + } + /** * Get the current wave number. * @return the current wave number diff --git a/src/main/java/com/garbagemule/MobArena/waves/WaveParser.java b/src/main/java/com/garbagemule/MobArena/waves/WaveParser.java index a3dba27a..9219aefe 100644 --- a/src/main/java/com/garbagemule/MobArena/waves/WaveParser.java +++ b/src/main/java/com/garbagemule/MobArena/waves/WaveParser.java @@ -15,28 +15,14 @@ import com.garbagemule.MobArena.waves.ability.AbilityManager; import com.garbagemule.MobArena.waves.enums.WaveBranch; import com.garbagemule.MobArena.waves.enums.WaveType; -import com.garbagemule.MobArena.waves.types.BossWave; -import com.garbagemule.MobArena.waves.types.DefaultWave; -import com.garbagemule.MobArena.waves.types.SpecialWave; -import com.garbagemule.MobArena.waves.types.SupplyWave; -import com.garbagemule.MobArena.waves.types.SwarmWave; -import com.garbagemule.MobArena.waves.types.UpgradeWave; +import com.garbagemule.MobArena.waves.types.*; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.TreeSet; +import java.util.*; import java.util.stream.Collectors; public class WaveParser @@ -98,6 +84,9 @@ public static Wave parseWave(Arena arena, String name, ConfigurationSection conf case BOSS: result = parseBossWave(arena, name, config); break; + case COMBO: + result = parseComboWave(arena, name, config); + break; } // Grab the branch-specific nodes. @@ -122,7 +111,7 @@ public static Wave parseWave(Arena arena, String name, ConfigurationSection conf // Potion effects List effects = getPotionEffects(arena, name, config); - // Recurrent must have priority + frequency, single must have firstWave + // Recurrent must have priority + frequency, single must have firstWave, combo doesn't need this if (branch == WaveBranch.RECURRENT) { if (priority <= 0) { throw new ConfigError("Missing or invalid 'priority' node for recurrent wave " + name + " of arena " + arena.configName()); @@ -140,6 +129,7 @@ public static Wave parseWave(Arena arena, String name, ConfigurationSection conf result.setFirstWave(firstWave); result.setPriority(priority); result.setFrequency(frequency); + result.setShadowBlacklist(Optional.ofNullable(config.getString("noshadow")).orElse("")); // And the multipliers. result.setHealthMultiplier(healthMultiplier); @@ -350,6 +340,21 @@ private static Wave parseBossWave(Arena arena, String name, ConfigurationSection return result; } + private static Wave parseComboWave(Arena arena, String name, ConfigurationSection config) { + ConfigurationSection waveAConfig = config.getConfigurationSection("waveA"); + if (waveAConfig == null) { + throw new ConfigError("No waveA defined in wave " + name + " of arena " + arena.configName() + "."); + } + Wave waveA = parseWave(arena, name, waveAConfig, WaveBranch.COMBO); + ConfigurationSection waveBConfig = config.getConfigurationSection("waveB"); + if (waveBConfig == null) { + throw new ConfigError("No waveB defined in wave " + name + " of arena " + arena.configName() + "."); + } + Wave waveB = parseWave(arena, name, waveBConfig, WaveBranch.COMBO); + + return new ComboWave(waveA, waveB); + } + /** * Scan the ConfigSection for a "monster" (singular) node, which * must be exactly a single monster. diff --git a/src/main/java/com/garbagemule/MobArena/waves/WaveUtils.java b/src/main/java/com/garbagemule/MobArena/waves/WaveUtils.java index 53320dc8..f6910a83 100644 --- a/src/main/java/com/garbagemule/MobArena/waves/WaveUtils.java +++ b/src/main/java/com/garbagemule/MobArena/waves/WaveUtils.java @@ -8,13 +8,29 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; +import java.util.*; public class WaveUtils { + /** + * Test if a wave gets blocked by a given blacklist + */ + public static boolean waveBlocked(Wave wave, String blacklist) { + if (blacklist == null) { + return true; + } + if (blacklist.equals("*")) { + return false; + } + String[] blacklistedWaves = blacklist.split(", ?"); + for (String waveName : blacklistedWaves) { + if (waveName.equalsIgnoreCase(wave.getName())) { + return false; + } + } + return true; + } + /** * Get all the spawnpoints that have players nearby. */ diff --git a/src/main/java/com/garbagemule/MobArena/waves/enums/WaveBranch.java b/src/main/java/com/garbagemule/MobArena/waves/enums/WaveBranch.java index d3f87923..eb0089b3 100644 --- a/src/main/java/com/garbagemule/MobArena/waves/enums/WaveBranch.java +++ b/src/main/java/com/garbagemule/MobArena/waves/enums/WaveBranch.java @@ -19,6 +19,13 @@ public boolean matches(int wave, Wave w) { } return ((wave - w.getFirstWave()) % w.getFrequency() == 0); } + }, + + COMBO { + @Override + public boolean matches(int wave, Wave w) { + return false; // doesn't get used anyway on COMBO branch + } }; public abstract boolean matches(int wave, Wave w); diff --git a/src/main/java/com/garbagemule/MobArena/waves/enums/WaveType.java b/src/main/java/com/garbagemule/MobArena/waves/enums/WaveType.java index 30b9774b..94cf67a2 100644 --- a/src/main/java/com/garbagemule/MobArena/waves/enums/WaveType.java +++ b/src/main/java/com/garbagemule/MobArena/waves/enums/WaveType.java @@ -45,6 +45,13 @@ public void announce(Arena arena, int wave) { public void announce(Arena arena, int wave) { arena.announce(Msg.WAVE_UPGRADE, "" + wave); } + }, + + COMBO { + @Override + public void announce(Arena arena, int wave) { + // announce waveA in MASpawnThread.handleWave(Wave, int, boolean) + } }; public abstract void announce(Arena arena, int wave); diff --git a/src/main/java/com/garbagemule/MobArena/waves/types/ComboWave.java b/src/main/java/com/garbagemule/MobArena/waves/types/ComboWave.java new file mode 100644 index 00000000..c34691c3 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/waves/types/ComboWave.java @@ -0,0 +1,61 @@ +package com.garbagemule.MobArena.waves.types; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.waves.AbstractWave; +import com.garbagemule.MobArena.waves.MACreature; +import com.garbagemule.MobArena.waves.Wave; +import com.garbagemule.MobArena.waves.WaveUtils; +import com.garbagemule.MobArena.waves.enums.WaveType; +import org.bukkit.Location; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ComboWave extends AbstractWave { + + private final Wave waveA; + private final Wave waveB; + + public ComboWave(Wave waveA, Wave waveB) { + this.waveA = waveA; + this.waveB = waveB; + this.setType(WaveType.COMBO); + } + + public Wave getWaveA() { + return waveA; + } + + public Wave getWaveB() { + return waveB; + } + + @Override + public Map getMonstersToSpawn(int wave, int playerCount, Arena arena) { + Map monstersA = this.waveA.getMonstersToSpawn(wave, playerCount, arena); + Map monstersB = this.waveB.getMonstersToSpawn(wave, playerCount, arena); + + Map monstersCombined = new HashMap<>(); + monstersCombined.putAll(monstersA); + monstersCombined.putAll(monstersB); + + return monstersCombined; + } + + public List getSpawnpoints(Arena arena) { + List spawnpointsA = waveA.getSpawnpoints(arena); + List spawnpointsB = waveB.getSpawnpoints(arena); + + List spawnpointsCombined = super.getSpawnpoints(arena); + spawnpointsCombined.addAll(spawnpointsA); + spawnpointsCombined.addAll(spawnpointsB); + + return WaveUtils.getValidSpawnpoints(arena, spawnpointsCombined, arena.getPlayersInArena()); + } + + @Override + public Wave copy() { + return new ComboWave(getWaveA(), getWaveB()); + } +}