diff --git a/TODO.md b/TODO.md index 9b03c2f..7e04103 100644 --- a/TODO.md +++ b/TODO.md @@ -1,30 +1,78 @@ -# TODO +# Objective + +[X] Mobs spawn in waves +[X] Mobs move towards goal +[ ] Mobs carry bomb to goal +[X] Mobs arm bomb +[X] Bomb explodes + +# UX + +[X] Wave boss bar +[X] Mob count boss bar +[X] Stage titles +[ ] EXPLOSIONS +[ ] Colored titles +[ ] Clickable /ready in chat +[ ] Sidebar + +# Social + +[ ] Automatic VC groups for games +[ ] DiscordSRV +[ ] Plan stats +[ ] /invite friends to games +[ ] /voterestart + +# Mechanics [ ] 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 +[ ] Mob tracking should prioritize bomb +[ ] Mobs recover dropped bombs +[ ] Bomb carriers are slower +[ ] Bonus coins for complete coin pickup +[ ] Infinite weapons + armor +[ ] Ammo/health spawns # Scripted waves +[X] Spawn in batches +[X] Randomized spawn locations +[X] Weighted distributions +[X] Batch overlap +[ ] Scripted batch overlap/timings +[ ] Scripted spawn locations [ ] Bosses -[ ] Batch overlap/timing + +# Mapping + +[ ] Load arenas from config file +[ ] Live map editing # Game lifecycle -[ ] Return to lobby -[ ] Join game via sign/commands/something -[ ] Bonus coins for complete coin pickup +[ ] /list arenas and games +[ ] /start a game on an arena +[ ] /join games +[X] /ready +[ ] /leave games +[ ] /restart games +[X] Spectator mode on death +[X] Player readiness starts countdown +[ ] Game is automatically closed some time after game over +[ ] Return to lobby on leave/close +[ ] Instancing -# Cleanup +# Powerups -[ ] Generic countdown mechanic -[ ] +[ ] Coin pickup range +[ ] Coin boost +[ ] Knockback on weapons +[ ] Damage boost +[ ] Speed boost +[ ] Health boost +[ ] Repair barriers + +# Fantasy + +[ ] Totems/turrets/stationary weapons diff --git a/src/main/java/gg/malloc/defense/GameEventHandler.java b/src/main/java/gg/malloc/defense/GameEventHandler.java index a13bb1e..2dd2007 100644 --- a/src/main/java/gg/malloc/defense/GameEventHandler.java +++ b/src/main/java/gg/malloc/defense/GameEventHandler.java @@ -7,6 +7,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityCombustEvent; +import org.bukkit.event.entity.EntityTargetEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.entity.Player; @@ -32,6 +33,11 @@ public class GameEventHandler implements Listener { evt.setCancelled(true); } + @EventHandler + public void onEntityTarget(EntityTargetEvent evt) { + m_runner.handleEntityRetargeting(evt); + } + @EventHandler public void onEntityDamage(EntityDamageEvent evt) { if (evt.getEntity() instanceof Player) { diff --git a/src/main/java/gg/malloc/defense/MemoryArena.java b/src/main/java/gg/malloc/defense/MemoryArena.java index 891b17f..18e407e 100644 --- a/src/main/java/gg/malloc/defense/MemoryArena.java +++ b/src/main/java/gg/malloc/defense/MemoryArena.java @@ -8,13 +8,15 @@ import org.bukkit.World; public class MemoryArena implements Arena { Spawnpoint[] m_spawnpoints; + Spawnpoint m_bombTarget; World m_world; String m_name; - public MemoryArena(String name, World world, Spawnpoint[] spawnpoints) { + public MemoryArena(String name, World world, Spawnpoint[] spawnpoints, Spawnpoint bombTarget) { m_world = world; m_spawnpoints = spawnpoints; m_name = name; + m_bombTarget = bombTarget; } @Override @@ -31,4 +33,9 @@ public class MemoryArena implements Arena { public World getWorld() { return m_world; } + + @Override + public Spawnpoint bombTarget() { + return m_bombTarget; + } } diff --git a/src/main/java/gg/malloc/defense/Plugin.java b/src/main/java/gg/malloc/defense/Plugin.java index ea07361..5891526 100644 --- a/src/main/java/gg/malloc/defense/Plugin.java +++ b/src/main/java/gg/malloc/defense/Plugin.java @@ -6,6 +6,7 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.World; +import org.bukkit.WorldCreator; import java.util.logging.Logger; import java.util.logging.Level; @@ -24,6 +25,7 @@ import gg.malloc.defense.engine.GameRunner; import gg.malloc.defense.commands.AddPlayerCommand; import gg.malloc.defense.commands.SetStageCommand; +import gg.malloc.defense.commands.PlayerReadyCommand; public class Plugin extends JavaPlugin { ArrayList m_arenas = new ArrayList<>(); @@ -65,6 +67,7 @@ public class Plugin extends JavaPlugin { getCommand("setstage").setExecutor(new SetStageCommand(this)); getCommand("addplayer").setExecutor(new AddPlayerCommand(this)); getCommand("debuginfo").setExecutor(new DebuginfoCommand(this)); + getCommand("ready").setExecutor(new PlayerReadyCommand(this)); } public GameRunner getRunnerForWorld(World world) { @@ -81,14 +84,16 @@ public class Plugin extends JavaPlugin { void setupDemoGame() { getLogger().info("Setting up demo data'"); - World testWorld = getServer().getWorld("world"); - 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); + World testWorld = getServer().getWorld("quarry"); + if (testWorld == null) { + testWorld = new WorldCreator("quarry").generateStructures(false).createWorld(); } - m_arenas.add(new MemoryArena("Test Arena", testWorld, spawnpoints)); + Spawnpoint[] spawnpoints = new Spawnpoint[3]; + spawnpoints[0] = new TestSpawn(new Location(testWorld, -15, 80, -46)); + spawnpoints[1] = new TestSpawn(new Location(testWorld, -1, 80, -45)); + spawnpoints[2] = new TestSpawn(new Location(testWorld, 12, 81, -42)); + Spawnpoint bombTarget = new TestSpawn(new Location(testWorld, -20, 80, 31)); + m_arenas.add(new MemoryArena("Test Arena", testWorld, spawnpoints, bombTarget)); m_games.add(new ScaledWaves()); } } diff --git a/src/main/java/gg/malloc/defense/commands/PlayerReadyCommand.java b/src/main/java/gg/malloc/defense/commands/PlayerReadyCommand.java new file mode 100644 index 0000000..d6fa338 --- /dev/null +++ b/src/main/java/gg/malloc/defense/commands/PlayerReadyCommand.java @@ -0,0 +1,33 @@ +package gg.malloc.defense.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import gg.malloc.defense.engine.GameRunner; + +import gg.malloc.defense.Plugin; + +public class PlayerReadyCommand implements CommandExecutor { + Plugin m_plugin; + + public PlayerReadyCommand(Plugin plugin) { + m_plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { + if (sender instanceof Player) { + Player player = (Player)sender; + GameRunner runner = m_plugin.getRunnerForWorld(player.getLocation().getWorld()); + runner.addPlayer(player); + runner.togglePlayerReady(player); + return true; + } else { + sender.sendMessage("You must be a player to use this command."); + return false; + } + } +} + diff --git a/src/main/java/gg/malloc/defense/engine/GameRunner.java b/src/main/java/gg/malloc/defense/engine/GameRunner.java index 4e212f7..e8e827d 100644 --- a/src/main/java/gg/malloc/defense/engine/GameRunner.java +++ b/src/main/java/gg/malloc/defense/engine/GameRunner.java @@ -10,14 +10,18 @@ import gg.malloc.defense.Plugin; import java.util.logging.Logger; import org.bukkit.Bukkit; +import org.bukkit.Sound; import org.bukkit.World; import org.bukkit.boss.BarColor; import org.bukkit.boss.BarStyle; import org.bukkit.boss.BossBar; import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.entity.EntityTargetEvent; import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.ArmorStand; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; @@ -30,17 +34,23 @@ public class GameRunner { BossBar m_gameBar = Bukkit.createBossBar("Malloc Defense", BarColor.PURPLE, BarStyle.SOLID); BossBar m_waveBar = Bukkit.createBossBar("Malloc Defense", BarColor.PURPLE, BarStyle.SOLID); + BossBar m_bombBar = Bukkit.createBossBar("Bomb HP", BarColor.RED, BarStyle.SOLID); + BukkitTask m_countdownTask; MobManager m_mobs; WaveManager m_waves; PlayerManager m_players; + LivingEntity m_bombTarget = null; + int m_bombHP = 100; + Logger m_log; public enum Stage { Idle, Warmup, + Countdown, Playing, GameOver } @@ -104,6 +114,7 @@ public class GameRunner { m_stage = Stage.Idle; m_gameBar.setVisible(true); m_waveBar.setVisible(false); + m_bombBar.setVisible(false); m_mobs = new MobManager(m_game); m_waves = new WaveManager(m_game); m_players = new PlayerManager(); @@ -112,8 +123,30 @@ public class GameRunner { int m_warmupCountdown = 0; + public void handleEntityRetargeting(EntityTargetEvent evt) { + if (m_mobs.contains(evt.getEntity()) && evt.getReason() == EntityTargetEvent.TargetReason.FORGOT_TARGET) { + evt.setTarget(m_bombTarget); + } + } + public void handleEntityDamage(EntityDamageEvent evt) { - m_mobs.handleEntityDamage(evt); + if (evt.getEntity() == m_bombTarget && evt instanceof EntityDamageByEntityEvent) { + EntityDamageByEntityEvent entityEvt = (EntityDamageByEntityEvent)evt; + if (m_mobs.contains(entityEvt.getDamager())) { + entityEvt.getDamager().setGlowing(true); + m_bombHP -= 1; + broadcastMessage("The bomb has been struck!"); + m_arena.getWorld().playSound(m_bombTarget.getLocation(), Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR, 1.5f, 0.9f); + updateMobBars(); + if (m_bombHP <= 0) { + m_arena.getWorld().strikeLightningEffect(m_bombTarget.getLocation()); + requestTransition(Stage.GameOver); + } + } + evt.setCancelled(true); + } else { + m_mobs.handleEntityDamage(evt); + } } private void countdownTick() { @@ -130,9 +163,14 @@ public class GameRunner { } private boolean enterIdle() { + m_bombHP = 100; m_waves.reset(); m_mobs.clear(); m_players.requestTransitionForAll(PlayerManager.State.Idle); + if (m_bombTarget != null) { + m_bombTarget.remove(); + m_bombTarget = null; + } if (m_countdownTask != null) { m_countdownTask.cancel(); m_countdownTask = null; @@ -142,25 +180,54 @@ public class GameRunner { private boolean enterWarmup() { m_waves.next(); - m_warmupCountdown = 10; for(Player p : m_players.getPlayers()) { if (m_players.requestTransition(p, PlayerManager.State.Playing)) { p.teleport(m_arena.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); } + m_players.setReady(p, false); } broadcastTitle("Warmup", "Prepare yourself for wave " + m_waves.currentWaveNum()); m_mobs.clear(); + return true; + } + + public void togglePlayerReady(Player p) { + setPlayerReady(p, !m_players.isReady(p)); + } + + public void setPlayerReady(Player p, boolean isReady) { + m_players.setReady(p, isReady); + if (m_players.isEveryoneReady()) { + requestTransition(Stage.Countdown); + } + } + + private boolean enterCountdown() { + m_warmupCountdown = 10; countdownTick(); return true; } private boolean enterPlaying() { m_log.info("Starting wave " + m_waves.currentWaveNum()); + if (m_bombTarget == null) { + m_bombTarget = (LivingEntity)m_arena.getWorld().spawnEntity(m_arena.bombTarget().getLocation(), EntityType.ARMOR_STAND); + } + ArmorStand bombStand = (ArmorStand)m_bombTarget; + bombStand.setCustomName("Bomb Target"); + bombStand.setVisible(true); + bombStand.setCustomNameVisible(true); + bombStand.setGlowing(true); + for(Player p : m_players.getPlayers()) { + if (m_players.requestTransition(p, PlayerManager.State.Playing)) { + p.teleport(m_arena.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); + } + } spawnNextBatch(); broadcastTitle("Wave " + m_waves.currentWaveNum()); return true; } - + private boolean enterGameOver() { broadcastTitle("Game Over!"); if (m_countdownTask != null) { @@ -179,18 +246,30 @@ public class GameRunner { switch(m_stage) { case Idle: m_gameBar.setProgress(1.0); - m_gameBar.setTitle("Waiting for playres..."); + m_gameBar.setTitle("Waiting for players..."); m_gameBar.setColor(BarColor.PURPLE); m_waveBar.setVisible(false); + m_bombBar.setVisible(false); break; case Warmup: + m_gameBar.setProgress(m_waves.progress()); + m_gameBar.setTitle("Wave " + m_waves.currentWaveNum() + " / " + m_game.getWaveCount()); + m_gameBar.setColor(BarColor.PURPLE); + m_waveBar.setVisible(true); + m_waveBar.setColor(BarColor.BLUE); + m_waveBar.setTitle("Warmup - Waiting for players to get ready..."); + m_waveBar.setProgress(m_players.readyProgress()); + m_bombBar.setVisible(false); + break; + case Countdown: m_gameBar.setProgress(m_waves.progress()); m_gameBar.setTitle("Wave " + m_waves.currentWaveNum() + " / " + m_game.getWaveCount()); m_gameBar.setColor(BarColor.PURPLE); m_waveBar.setVisible(true); m_waveBar.setColor(BarColor.YELLOW); - m_waveBar.setTitle("Warmup"); + m_waveBar.setTitle("Wave starting!"); m_waveBar.setProgress((double)m_warmupCountdown / (double)10); + m_bombBar.setVisible(false); break; case Playing: m_gameBar.setProgress(m_waves.progress()); @@ -204,19 +283,22 @@ public class GameRunner { } else { m_waveBar.setVisible(false); } + m_bombBar.setVisible(true); + m_bombBar.setProgress((double)m_bombHP / (double)100); break; case GameOver: m_gameBar.setColor(BarColor.RED); m_gameBar.setProgress(1.0); m_gameBar.setTitle("Game Over!"); m_waveBar.setVisible(false); + m_bombBar.setVisible(false); break; } } private void spawnNextBatch() { broadcastMessage("Spawning batch " + m_waves.currentBatchNum()); - Spawner spawner = new GameSpawner(m_arena, m_mobs); + Spawner spawner = new GameSpawner(m_arena, m_mobs, m_players, m_bombTarget); m_waves.currentWave().spawnBatch(spawner, m_waves.currentBatchNum()); updateMobBars(); } @@ -265,6 +347,8 @@ public class GameRunner { return enterIdle(); case Warmup: return enterWarmup(); + case Countdown: + return enterCountdown(); case Playing: return enterPlaying(); case GameOver: @@ -278,7 +362,9 @@ public class GameRunner { case Idle: return to == Stage.Warmup; case Warmup: - return to == Stage.Playing || to == Stage.Idle || to == Stage.GameOver; + return to == Stage.Playing || to == Stage.Idle || to == Stage.Countdown ; + case Countdown: + return to == Stage.Playing || to == Stage.Idle || to == Stage.Warmup; case Playing: return to == Stage.Warmup || to == Stage.GameOver || to == Stage.Idle; case GameOver: @@ -306,6 +392,7 @@ public class GameRunner { m_players.addPlayer(p); m_gameBar.addPlayer(p); m_waveBar.addPlayer(p); + m_bombBar.addPlayer(p); if (m_stage == Stage.Idle || m_stage == Stage.Warmup) { if (m_players.requestTransition(p, PlayerManager.State.Playing)) { p.teleport(m_arena.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); @@ -346,5 +433,4 @@ public class GameRunner { void registerSpawnedMob(Entity entity) { m_mobs.addEntity(entity); } - } diff --git a/src/main/java/gg/malloc/defense/engine/GameSpawner.java b/src/main/java/gg/malloc/defense/engine/GameSpawner.java index 2a5985b..a5fc118 100644 --- a/src/main/java/gg/malloc/defense/engine/GameSpawner.java +++ b/src/main/java/gg/malloc/defense/engine/GameSpawner.java @@ -7,15 +7,20 @@ import gg.malloc.defense.model.Arena; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Mob; public class GameSpawner implements Spawner { Arena m_arena; MobManager m_manager; + PlayerManager m_players; + LivingEntity m_bombTarget; int m_spawnIdx = 0; - public GameSpawner(Arena arena, MobManager manager) { + public GameSpawner(Arena arena, MobManager manager, PlayerManager players, LivingEntity bombTarget) { m_arena = arena; m_manager = manager; + m_players = players; + m_bombTarget = bombTarget; } @Override @@ -28,6 +33,7 @@ public class GameSpawner implements Spawner { livingMob.setRemoveWhenFarAway(false); m_manager.addEntity(newMob); m_spawnIdx += 1; + ((Mob)newMob).setTarget(m_bombTarget); return newMob; } } diff --git a/src/main/java/gg/malloc/defense/engine/MobManager.java b/src/main/java/gg/malloc/defense/engine/MobManager.java index a7ab9ff..b5de507 100644 --- a/src/main/java/gg/malloc/defense/engine/MobManager.java +++ b/src/main/java/gg/malloc/defense/engine/MobManager.java @@ -27,6 +27,10 @@ public class MobManager { } } + public boolean contains(Entity entity) { + return m_livingMobs.contains(entity); + } + public boolean killMob(Entity entity) { if (m_livingMobs.contains(entity)) { m_killedMobs += 1; diff --git a/src/main/java/gg/malloc/defense/engine/PlayerManager.java b/src/main/java/gg/malloc/defense/engine/PlayerManager.java index 1c1bf39..3023561 100644 --- a/src/main/java/gg/malloc/defense/engine/PlayerManager.java +++ b/src/main/java/gg/malloc/defense/engine/PlayerManager.java @@ -11,6 +11,7 @@ import java.util.Collection; public class PlayerManager { HashMap m_playerStates = new HashMap<>(); + HashMap m_playerReadyStates = new HashMap<>(); public enum State { Idle, @@ -70,6 +71,34 @@ public class PlayerManager { public void addPlayer(Player player) { //m_log.info("Adding player " + player); m_playerStates.put(player, State.Idle); + m_playerReadyStates.put(player, false); + } + + public boolean isReady(Player player) { + return m_playerReadyStates.get(player); + } + + public void setReady(Player player, boolean ready) { + m_playerReadyStates.put(player, ready); + } + + public double readyProgress() { + int readyNum = 0; + for(boolean b : m_playerReadyStates.values()) { + if (b) { + readyNum += 1; + } + } + return (double)readyNum / (double)m_playerReadyStates.size(); + } + + public boolean isEveryoneReady() { + for(boolean b : m_playerReadyStates.values()) { + if (!b) { + return false; + } + } + return true; } public boolean removePlayer(Player player) { diff --git a/src/main/java/gg/malloc/defense/model/Arena.java b/src/main/java/gg/malloc/defense/model/Arena.java index 1a39c59..11e5265 100644 --- a/src/main/java/gg/malloc/defense/model/Arena.java +++ b/src/main/java/gg/malloc/defense/model/Arena.java @@ -6,4 +6,5 @@ public interface Arena { World getWorld(); String name(); Spawnpoint[] spawnpoints(); + Spawnpoint bombTarget(); } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 663fca6..0737546 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -10,3 +10,5 @@ commands: description: Adds a player to a game debuginfo: description: Unknowable powers + ready: + description: Mark yourself as ready for the next wave