diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..9b03c2f --- /dev/null +++ b/TODO.md @@ -0,0 +1,30 @@ +# TODO + +[ ] Coin drops +[ ] Basic setup commands +[ ] Config file +[ ] Spectator mode +[ ] Commands/tools to start warmup countdown/mark 'ready' +[ ] Upgrades +[ ] Plan stats +[ ] Live map editing +[ ] Instancing +[ ] DiscordSRV +[ ] Voice Chat +[ ] Player readiness starts countdown + +# Scripted waves + +[ ] Bosses +[ ] Batch overlap/timing + +# Game lifecycle + +[ ] Return to lobby +[ ] Join game via sign/commands/something +[ ] Bonus coins for complete coin pickup + +# Cleanup + +[ ] Generic countdown mechanic +[ ] diff --git a/src/main/java/gg/malloc/defense/GameEventHandler.java b/src/main/java/gg/malloc/defense/GameEventHandler.java index 9cb2fac..1b1a81a 100644 --- a/src/main/java/gg/malloc/defense/GameEventHandler.java +++ b/src/main/java/gg/malloc/defense/GameEventHandler.java @@ -4,27 +4,25 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.entity.PlayerQuitEvent; import org.bukkit.event.entity.EntityCombustEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.entity.Player; public class GameEventHandler implements Listener { - Plugin m_plugin; + GameRunner m_runner; - public GameEventHandler(Plugin plugin) { - m_plugin = plugin; + public GameEventHandler(GameRunner runner) { + m_runner = runner; } @EventHandler public void onEntityDeath(EntityDeathEvent evt) { - GameRunner runner = m_plugin.getRunnerForWorld(evt.getEntity().getLocation().getWorld()); - runner.handleEntityDeath(evt.getEntity()); + m_runner.handleEntityDeath(evt.getEntity()); } @EventHandler public void onPlayerQuit(PlayerQuitEvent evt) { - GameRunner runner = m_plugin.getRunnerForWorld(evt.getPlayer().getLocation().getWorld()); - runner.removePlayer(evt.getEntity()); + m_runner.removePlayer(evt.getPlayer()); } @EventHandler @@ -37,10 +35,11 @@ public class GameEventHandler implements Listener { if (evt.getEntity() instanceof Player) { Player player = (Player)evt.getEntity(); if (player.getHealth() - evt.getFinalDamage() <= 0) { - GameRunner runner = m_plugin.getRunnerForWorld(evt.getEntity().getLocation().getWorld()); evt.setCancelled(true); - runner.handlePlayerDeath(player); + m_runner.handlePlayerDeath(player); } + } else { + m_runner.handleEntityDamage(evt); } } } diff --git a/src/main/java/gg/malloc/defense/GameRunner.java b/src/main/java/gg/malloc/defense/GameRunner.java index f2539fa..9c4dbdd 100644 --- a/src/main/java/gg/malloc/defense/GameRunner.java +++ b/src/main/java/gg/malloc/defense/GameRunner.java @@ -5,39 +5,47 @@ import gg.malloc.defense.model.Arena; import gg.malloc.defense.model.Spawnpoint; import gg.malloc.defense.model.Spawner; import gg.malloc.defense.model.Wave; + import java.util.ArrayList; +import java.util.HashSet; +import java.util.logging.Logger; import org.bukkit.Bukkit; import org.bukkit.World; +import org.bukkit.GameMode; +import org.bukkit.attribute.Attribute; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.scheduler.BukkitTask; +import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; -import org.bukkit.boss.BarColor; -import org.bukkit.boss.BarStyle; -import org.bukkit.boss.BossBar; -import org.bukkit.GameMode; -import org.bukkit.attribute.Attribute; +import org.bukkit.scheduler.BukkitTask; public class GameRunner { - ArrayList m_spawnedMobs = new ArrayList(); + HashSet m_livingMobs = new HashSet<>(); + int m_createdMobs = 0; int m_killedMobs = 0; Arena m_arena; Game m_game; State m_state; - ArrayList m_players = new ArrayList<>(); - ArrayList m_livingPlayers = new ArrayList<>(); + HashSet m_players = new HashSet<>(); + HashSet m_livingPlayers = new HashSet<>(); Plugin m_plugin; BossBar m_gameBar = Bukkit.createBossBar("Malloc Defense", BarColor.PURPLE, BarStyle.SOLID); + BossBar m_waveBar = Bukkit.createBossBar("Malloc Defense", BarColor.PURPLE, BarStyle.SOLID); BukkitTask m_countdownTask; int m_currentWaveNum = 0; int m_currentBatch = 0; Wave m_currentWave = null; + Logger m_log; + enum State { Idle, Warmup, @@ -51,15 +59,26 @@ public class GameRunner { m_arena = arena; m_state = State.Idle; m_gameBar.setVisible(true); + m_waveBar.setVisible(false); + m_log = m_plugin.getLogger(); } int m_warmupCountdown = 0; + public void handleEntityDamage(EntityDamageEvent evt) { + Entity entity = evt.getEntity(); + m_log.info("Damage " + entity); + m_log.info("Living Mobs " + m_livingMobs); + if (m_livingMobs.contains(entity)) { + m_game.onMobDamaged(entity); + } + } + private void countdownTick() { if (m_warmupCountdown == 0) { requestTransition(State.Playing); } else { - m_gameBar.setProgress((double)m_warmupCountdown / (double)30); + updateMobBars(); broadcastMessage("Starting game in " + m_warmupCountdown); m_warmupCountdown--; m_countdownTask = m_plugin.getServer().getScheduler().runTaskLater(m_plugin, () -> { @@ -69,18 +88,21 @@ public class GameRunner { } private void clearMobs() { - for(Entity e : m_spawnedMobs) { + for(Entity e : m_livingMobs) { e.remove(); } - m_spawnedMobs.clear(); + m_livingMobs.clear(); + m_createdMobs = 0; + m_killedMobs = 0; } private boolean enterIdle() { broadcastMessage("Game state: Idle"); m_currentWaveNum = 0; - m_gameBar.setColor(BarColor.PURPLE); - m_gameBar.setProgress(1.0); - m_gameBar.setTitle("Idle"); + m_livingPlayers.clear(); + for(Player p : m_players) { + m_livingPlayers.add(p); + } if (m_countdownTask != null) { m_countdownTask.cancel(); m_countdownTask = null; @@ -90,83 +112,115 @@ public class GameRunner { } private boolean enterWarmup() { - broadcastMessage("Game state: Warmup"); + m_log.info("Game state: Warmup"); m_currentWaveNum += 1; m_currentWave = m_game.getWave(m_currentWaveNum); - m_gameBar.setColor(BarColor.YELLOW); - m_gameBar.setProgress(1.0); - m_gameBar.setTitle("Warmup"); - m_warmupCountdown = 30; + m_warmupCountdown = 10; for(Player p : m_players) { m_livingPlayers.add(p); } + broadcastTitle("Warmup", "Prepare yourself for wave " + m_currentWaveNum); clearMobs(); countdownTick(); return true; } private boolean enterPlaying() { - broadcastMessage("Game state: Playing"); - broadcastMessage("Starting wave " + m_currentWaveNum); + m_log.info("Game state: Playing"); + m_log.info("Starting wave " + m_currentWaveNum); m_currentBatch = 1; - m_gameBar.setColor(BarColor.GREEN); - m_gameBar.setProgress(0.0); - m_gameBar.setTitle("Playing"); spawnNextBatch(); + broadcastTitle("Wave " + m_currentWaveNum); return true; } private boolean enterGameOver() { - broadcastMessage("Game state: Game Over!"); - m_gameBar.setColor(BarColor.RED); - m_gameBar.setProgress(1.0); - m_gameBar.setTitle("Game Over!"); + broadcastTitle("Game Over!"); if (m_countdownTask != null) { m_countdownTask.cancel(); m_countdownTask = null; } - for(Player p : m_players) { - p.setGameMode(GameMode.ADVENTURE); - } clearMobs(); + for(Player p : m_players) { + removePlayer(p); + } return true; } - private void updateMobBar() { - m_gameBar.setTitle("Mobs remaining: " + (m_currentWave.totalMobCount() - m_killedMobs)); - m_gameBar.setProgress(m_killedMobs / m_currentWave.totalMobCount()); + private void updateMobBars() { + m_gameBar.setVisible(true); + switch(m_state) { + case Idle: + m_gameBar.setProgress(1.0); + m_gameBar.setTitle("Waiting for playres..."); + m_gameBar.setColor(BarColor.PURPLE); + m_waveBar.setVisible(false); + break; + case Warmup: + m_gameBar.setProgress((double)m_currentWaveNum / (double)m_game.getWaveCount()); + m_gameBar.setTitle("Wave " + m_currentWaveNum + " / " + m_game.getWaveCount()); + m_gameBar.setColor(BarColor.PURPLE); + m_waveBar.setVisible(true); + m_waveBar.setColor(BarColor.YELLOW); + m_waveBar.setTitle("Warmup"); + m_waveBar.setProgress((double)m_warmupCountdown / (double)10); + break; + case Playing: + m_gameBar.setProgress((double)m_currentWaveNum / (double)m_game.getWaveCount()); + m_gameBar.setTitle("Wave " + m_currentWaveNum + " / " + m_game.getWaveCount()); + m_gameBar.setColor(BarColor.PURPLE); + if (m_createdMobs > 0) { + m_waveBar.setVisible(true); + m_waveBar.setColor(BarColor.GREEN); + m_waveBar.setTitle("Mobs remaining: " + (m_createdMobs - m_killedMobs)); + m_waveBar.setProgress((double)m_killedMobs / (double)m_createdMobs); + } else { + m_waveBar.setVisible(false); + } + break; + case GameOver: + m_gameBar.setColor(BarColor.RED); + m_gameBar.setProgress(1.0); + m_gameBar.setTitle("Game Over!"); + m_waveBar.setVisible(false); + break; + } } private void spawnNextBatch() { broadcastMessage("Spawning batch " + m_currentBatch); - Spawner spawner = new GameSpawner(m_arena.spawnpoints()[0]); + Spawner spawner = new GameSpawner(m_arena.spawnpoints()); m_currentWave.spawnBatch(spawner, m_currentBatch); - updateMobBar(); + updateMobBars(); } public void handlePlayerDeath(Player player) { if (m_livingPlayers.contains(player)) { + m_log.info("Player has died in game" + player); m_livingPlayers.remove(player); + m_arena.getWorld().strikeLightningEffect(player.getLocation()); player.setGameMode(GameMode.SPECTATOR); if (m_livingPlayers.size() == 0) { broadcastMessage("Everyone is dead :("); requestTransition(State.GameOver); + } else { + m_log.info("Remaining players " + m_livingPlayers.size()); } } } public void handleEntityDeath(Entity entity) { - if (m_spawnedMobs.contains(entity)) { + if (m_livingMobs.contains(entity)) { broadcastMessage("Killed game entity " + entity); - m_spawnedMobs.remove(entity); + m_livingMobs.remove(entity); m_killedMobs += 1; - updateMobBar(); - if (m_spawnedMobs.size() == 0) { - broadcastMessage("Batch complete!"); + updateMobBars(); + if (m_livingMobs.size() <= 3) { + m_log.info("Starting next batch!"); if (m_currentBatch >= m_currentWave.batchCount()) { if (m_currentWaveNum >= m_game.getWaveCount()) { requestTransition(State.GameOver); - } else { + } else if (m_livingMobs.size() == 0) { requestTransition(State.Warmup); } } else { @@ -174,22 +228,31 @@ public class GameRunner { spawnNextBatch(); } } else { - broadcastMessage("Entities remaining: " + m_spawnedMobs.size()); + m_log.fine("Living mobs remaining: " + m_livingMobs.size()); } } } private boolean syncPlayer(Player player) { + m_log.fine("Synchronizing player " + player); World playerWorld = player.getLocation().getWorld(); World gameWorld = m_arena.getWorld(); m_gameBar.addPlayer(player); + m_waveBar.addPlayer(player); if (m_livingPlayers.contains(player)) { - player.setGameMode(GameMode.ADVENTURE); + m_log.fine("Player is alive, turning into adventure mode " + player); + if (player.getGameMode() != Bukkit.getDefaultGameMode()) { + player.setGameMode(Bukkit.getDefaultGameMode()); + player.teleport(gameWorld.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); + } player.setHealth(player.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue()); + player.setFoodLevel(10); } else { + m_log.fine("Player is dead, turning into spectator " + player); player.setGameMode(GameMode.SPECTATOR); } if (playerWorld != gameWorld) { + m_log.info("Teleporting player " + player); return player.teleport(gameWorld.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); } else { return false; @@ -229,22 +292,27 @@ public class GameRunner { public boolean requestTransition(State state) { if (!validateTransition(m_state, state)) { + m_log.warning("Attemped illegal transition: " + m_state + " -> " + state); return false; } if (attemptTransition(state)) { + updateMobBars(); + m_log.info("Game transition: " + m_state + " -> " + state); m_state = state; for(Player p : m_players) { syncPlayer(p); } return true; } + m_log.severe("Failed to complete transition: " + m_state + " -> " + state); return false; } public boolean addPlayer(Player p) { if (m_state == State.Idle || m_state == State.Warmup) { + m_log.info("Adding player " + p); m_players.add(p); - broadcastMessage("Added player " + p + " to game"); + broadcastMessage(p.getName() + " has joined the game"); syncPlayer(p); if (m_state == State.Idle) { requestTransition(State.Warmup); @@ -256,14 +324,27 @@ public class GameRunner { } public void removePlayer(Player p) { + m_log.info("Removing player " + p); m_gameBar.removePlayer(p); + m_waveBar.removePlayer(p); m_players.remove(p); m_livingPlayers.remove(p); + p.setGameMode(Bukkit.getDefaultGameMode()); if (m_players.size() == 0) { requestTransition(State.Idle); } } + void broadcastTitle(String title) { + broadcastTitle(title, ""); + } + + void broadcastTitle(String title, String subtitle) { + for(Player p : m_players) { + p.sendTitle(title, subtitle, 10, 70, 20); + } + } + void broadcastMessage(String string) { World world = m_arena.getWorld(); for(Player p : world.getPlayers()) { @@ -272,22 +353,28 @@ public class GameRunner { } void registerSpawnedMob(Entity entity) { - m_spawnedMobs.add(entity); + m_log.fine("Registered new mob " + entity); + m_livingMobs.add(entity); + m_createdMobs += 1; } private class GameSpawner implements Spawner { - Spawnpoint m_spawnpoint; - - public GameSpawner(Spawnpoint spawnpoint) { - m_spawnpoint = spawnpoint; + Spawnpoint[] m_spawnpoints; + int m_spawnIdx = 0; + + public GameSpawner(Spawnpoint[] spawnpoints) { + m_spawnpoints = spawnpoints; } @Override public Entity spawnMob(EntityType type) { - Entity newMob = m_arena.getWorld().spawnEntity(m_spawnpoint.getLocation(), type); + m_log.fine("Spawning " + type + " at " + m_spawnpoints[m_spawnIdx]); + Entity newMob = m_arena.getWorld().spawnEntity(m_spawnpoints[m_spawnIdx].getLocation(), type); LivingEntity livingMob = (LivingEntity)newMob; livingMob.setRemoveWhenFarAway(false); registerSpawnedMob(newMob); + m_spawnIdx += 1; + m_spawnIdx %= m_spawnpoints.length; return newMob; } } diff --git a/src/main/java/gg/malloc/defense/Plugin.java b/src/main/java/gg/malloc/defense/Plugin.java index 28e2607..e77f77d 100644 --- a/src/main/java/gg/malloc/defense/Plugin.java +++ b/src/main/java/gg/malloc/defense/Plugin.java @@ -6,7 +6,9 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.World; -import org.bukkit.plugin.PluginLogger; + +import java.util.logging.Logger; +import java.util.logging.Level; import java.util.ArrayList; import java.util.HashMap; @@ -16,6 +18,7 @@ import gg.malloc.defense.model.Spawnpoint; import gg.malloc.defense.model.Game; import gg.malloc.defense.games.LinearGame; +import gg.malloc.defense.games.ScaledWaves; public class Plugin extends JavaPlugin { ArrayList m_arenas = new ArrayList<>(); @@ -45,20 +48,18 @@ public class Plugin extends JavaPlugin { } } - PluginLogger m_log = new PluginLogger(this); - public void debuginfo() { - m_log.info("Debug Info:"); + getLogger().info("Debug Info:"); } @Override public void onEnable() { getLogger().info("Malloc Defense registered"); - setupTestGame(); + getLogger().setLevel(Level.FINEST); + setupDemoGame(); getCommand("setstage").setExecutor(new SetStageCommand(this)); getCommand("initgame").setExecutor(new InitGameCommand(this)); getCommand("debuginfo").setExecutor(new DebuginfoCommand(this)); - getServer().getPluginManager().registerEvents(new GameEventHandler(this), this); } public GameRunner getRunnerForWorld(World world) { @@ -68,15 +69,21 @@ public class Plugin extends JavaPlugin { } else { ret = new GameRunner(this, m_games.get(0), m_arenas.get(0)); m_runningGames.put(world, ret); + getServer().getPluginManager().registerEvents(new GameEventHandler(ret), this); } return ret; } - void setupTestGame() { + void setupDemoGame() { + getLogger().info("Setting up demo data'"); World testWorld = getServer().getWorld("world"); - Spawnpoint[] spawnpoints = new Spawnpoint[1]; - spawnpoints[0] = new TestSpawn(testWorld.getSpawnLocation()); + Spawnpoint[] spawnpoints = new Spawnpoint[4]; + Location spawnCenter = testWorld.getSpawnLocation(); + for(int i = 0; i < 4; i++) { + Location spawnLocation = spawnCenter.add(Math.random() * 3, 0, Math.random() * 3); + spawnpoints[i] = new TestSpawn(spawnLocation); + } m_arenas.add(new MemoryArena("Test Arena", testWorld, spawnpoints)); - m_games.add(new LinearGame()); + m_games.add(new ScaledWaves()); } } diff --git a/src/main/java/gg/malloc/defense/games/LinearGame.java b/src/main/java/gg/malloc/defense/games/LinearGame.java index 6fa07e9..565c85a 100644 --- a/src/main/java/gg/malloc/defense/games/LinearGame.java +++ b/src/main/java/gg/malloc/defense/games/LinearGame.java @@ -17,15 +17,6 @@ public class LinearGame implements Game { m_zombiesPerBatch = zombiesPerBatch; } - @Override - public int totalMobCount() { - int ret = 0; - for(int i = 1; i <= m_batches; i++) { - ret += i * m_zombiesPerBatch; - } - return ret; - } - @Override public int batchCount() { return m_batches; diff --git a/src/main/java/gg/malloc/defense/games/ScaledWaves.java b/src/main/java/gg/malloc/defense/games/ScaledWaves.java new file mode 100644 index 0000000..8391f58 --- /dev/null +++ b/src/main/java/gg/malloc/defense/games/ScaledWaves.java @@ -0,0 +1,79 @@ +package gg.malloc.defense.games; + +import gg.malloc.defense.model.Wave; +import gg.malloc.defense.model.Game; +import gg.malloc.defense.model.Spawner; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; + +import java.util.HashMap; + +public class ScaledWaves implements Game { + @Override + public int getWaveCount() { + return 10; + } + + @Override + public Wave getWave(int waveNumber) { + HashMap weights = new HashMap<>(); + if (waveNumber <= 3) { + weights.put(EntityType.ZOMBIE, 1.0); + } else if (waveNumber <= 5) { + weights.put(EntityType.ZOMBIE, 0.6); + weights.put(EntityType.SPIDER, 0.1); + weights.put(EntityType.SKELETON, 0.3); + } else if (waveNumber <= 7) { + weights.put(EntityType.ZOMBIE, 0.7); + weights.put(EntityType.SKELETON, 0.3); + } else { + weights.put(EntityType.PILLAGER, 0.2); + weights.put(EntityType.ZOMBIE, 0.8); + } + boolean hasRavager = waveNumber >= 9; + return new MobWave(weights, waveNumber * 3, 3, hasRavager); + } + + private class MobWave implements Wave { + HashMap m_spawnWeights; + int m_batches; + int m_mobsPerBatch; + boolean m_hasRavager; + + MobWave(HashMap weights, int totalCount, int batches, boolean hasRavager) { + m_batches = batches; + m_mobsPerBatch = Math.max(1, totalCount / batches); + m_spawnWeights = weights; + m_hasRavager = hasRavager; + } + + @Override + public int batchCount() { + return m_batches; + } + + @Override + public void spawnBatch(Spawner spawner, int batch) { + assert(m_mobsPerBatch > 0); + for(int i = 0; i < m_mobsPerBatch; i++) { + EntityType selectedType = null; + double random = Math.random() * 1.0; + for(EntityType type : m_spawnWeights.keySet()) { + random -= m_spawnWeights.get(type); + if (random <= 0.0d) { + selectedType = type; + break; + } + } + assert(selectedType != null); + Entity newMob = spawner.spawnMob(selectedType); + newMob.setCustomName("Mob " + i + "/" + m_mobsPerBatch); + } + if (m_hasRavager) { + Entity newMob = spawner.spawnMob(EntityType.RAVAGER); + newMob.setCustomName("RAVAGER"); + } + } + } +} diff --git a/src/main/java/gg/malloc/defense/model/Game.java b/src/main/java/gg/malloc/defense/model/Game.java index d264502..cdbb2d5 100644 --- a/src/main/java/gg/malloc/defense/model/Game.java +++ b/src/main/java/gg/malloc/defense/model/Game.java @@ -1,6 +1,8 @@ package gg.malloc.defense.model; +import org.bukkit.entity.Entity; public interface Game { int getWaveCount(); Wave getWave(int waveNumber); + default void onMobDamaged(Entity entity) {} } diff --git a/src/main/java/gg/malloc/defense/model/Wave.java b/src/main/java/gg/malloc/defense/model/Wave.java index a5757d0..1d1f14d 100644 --- a/src/main/java/gg/malloc/defense/model/Wave.java +++ b/src/main/java/gg/malloc/defense/model/Wave.java @@ -2,6 +2,5 @@ package gg.malloc.defense.model; public interface Wave { int batchCount(); - int totalMobCount(); void spawnBatch(Spawner spawner, int batch); }