diff --git a/TODO.md b/TODO.md
index ffb1edd..77fb4eb 100644
--- a/TODO.md
+++ b/TODO.md
@@ -12,18 +12,22 @@
[X] Config reload
[X] Leave games
[X] Join games
-[ ] Lobby with instructions
-[ ] Drop back to lobby on game over
-[ ] Grist drops
-[ ] Item shoppes
+[X] Drop back to lobby on game over
+[X] Grist drops
+[ ] Chat colors + clickables
+[ ] Title colors
[ ] Command tab completion
+[ ] Lobby with instructions
+[ ] Item shoppes
+[ ] Mob category AI
+[ ] Indestructible weapons/armor
+[ ] Never hungry
# Scaled waves
[ ] Limit ranged mobs and Ravagers to non-bomb-carrier state
[ ] Weaker mobs, more of them
-[ ] Mob categories
-[ ] Spawnpoint categories
+[ ] Mob AI categories
# UX
@@ -32,17 +36,19 @@
[X] Stage titles
[X] EXPLOSIONS
[X] Bomb model
-[ ] Pretty bomb model
-[ ] "Player $X is ready" message in chat
-[ ] Colored titles
+[X] "Player $X is ready" message in chat
+[X] Clickable /ready in chat
+[ ] Post-Round summary in chat
[ ] Clickable join links in /list
+[ ] Sidebar
+[ ] Coin pickup messages in action bar
+[ ] Target catches on fire more it is lit
+[ ] Colored titles
[ ] Clickable /leave action
-[ ] Clickable /ready in chat
[ ] Countdown while in warmup
[ ] Countdown shrinks w/ every /ready player
-[ ] Target catches on fire more it is lit
-[ ] Sidebar
[ ] List of mobs in next wave
+[ ] Pretty bomb model
# Social
@@ -71,6 +77,7 @@
[X] Randomized spawn locations
[X] Weighted distributions
[X] Batch overlap
+[ ] Spawnpoint categories
[ ] Scripted batch overlap/timings
[ ] Scripted spawn locations
[ ] Scripted waypoint paths
@@ -87,26 +94,29 @@
[X] /join games
[X] /ready
[X] /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
+[X] Game is automatically closed some time after game over
+[X] Return to lobby on leave/close
+[X] Restore health+hunger on respawn/game start
+[ ] /restart games
[ ] Instancing
-[ ] Restore health+hunger on respawn/game start
[ ] Respawn during games
[ ] Player revival items
+[ ] Clear inventory on join/leave
# Powerups
+[ ] Enchantments
+[ ] Better items
[ ] Coin pickup range
[ ] Coin boost
[ ] Knockback on weapons
[ ] Damage boost
[ ] Speed boost
[ ] Health boost
-[ ] Repair barriers
# Fantasy
[ ] Totems/turrets/stationary weapons
+[ ] Repair barriers
diff --git a/pom.xml b/pom.xml
index ba4740b..6372dd8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,12 @@
1.18.2-R0.1-SNAPSHOT
provided
+
+ de.tr7zw
+ item-nbt-api-plugin
+ 2.9.2
+ provided
+
diff --git a/src/main/java/gg/malloc/defense/Plugin.java b/src/main/java/gg/malloc/defense/Plugin.java
index 20b4657..6277363 100644
--- a/src/main/java/gg/malloc/defense/Plugin.java
+++ b/src/main/java/gg/malloc/defense/Plugin.java
@@ -9,9 +9,10 @@ import org.bukkit.entity.Player;
import org.bukkit.World;
import org.bukkit.WorldCreator;
-import org.bukkit.event.Listener;
-import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
import java.util.logging.Logger;
import java.util.logging.Level;
@@ -76,6 +77,21 @@ public class Plugin extends JavaPlugin {
getServer().getPluginManager().registerEvents(m_handler, this);
}
+ public void onDisable() {
+ getLogger().info("Unloading games...");
+ for(GameRunner runner : m_runningGames.values()) {
+ runner.requestTransition(GameRunner.Stage.Idle);
+ }
+ for(Player player : m_playerGames.keySet()) {
+ m_playerGames.get(player).removePlayer(player);
+ returnPlayerToLobby(player);
+ }
+ m_runningGames.clear();
+ m_playerGames.clear();
+ m_handler.clear();
+ m_arenas.clear();
+ }
+
public void reloadArenas() {
getLogger().info("Loading arenas...");
saveDefaultConfig();
@@ -87,6 +103,7 @@ public class Plugin extends JavaPlugin {
}
for(Player player : m_playerGames.keySet()) {
m_playerGames.get(player).removePlayer(player);
+ returnPlayerToLobby(player);
}
m_runningGames.clear();
m_playerGames.clear();
@@ -133,6 +150,15 @@ public class Plugin extends JavaPlugin {
return ret;
}
+ public void returnPlayerToLobby(Player p) {
+ Configuration config = new Configuration(getConfig());
+ World lobby = getServer().getWorld(config.getLobbyName());
+ if (lobby == null) {
+ lobby = new WorldCreator(config.getLobbyName()).generateStructures(false).createWorld();
+ }
+ p.teleport(lobby.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
+ }
+
public GameRunner getRunnerForPlayer(Player p) {
GameRunner ret = null;
if (m_playerGames.containsKey(p)) {
diff --git a/src/main/java/gg/malloc/defense/commands/JoinGameCommand.java b/src/main/java/gg/malloc/defense/commands/JoinGameCommand.java
index 588b1c9..2ed0c2b 100644
--- a/src/main/java/gg/malloc/defense/commands/JoinGameCommand.java
+++ b/src/main/java/gg/malloc/defense/commands/JoinGameCommand.java
@@ -49,7 +49,7 @@ public class JoinGameCommand implements TabExecutor {
}
m_plugin.addPlayerToArena(arenaName, player);
} else {
- sender.sendMessage("Only players may use htis command.");
+ sender.sendMessage("Only players may use this command.");
}
return true;
}
diff --git a/src/main/java/gg/malloc/defense/commands/LeaveGameCommand.java b/src/main/java/gg/malloc/defense/commands/LeaveGameCommand.java
index 139757e..17934ce 100644
--- a/src/main/java/gg/malloc/defense/commands/LeaveGameCommand.java
+++ b/src/main/java/gg/malloc/defense/commands/LeaveGameCommand.java
@@ -26,6 +26,7 @@ public class LeaveGameCommand implements CommandExecutor {
return true;
}
runner.removePlayer(player);
+ m_plugin.returnPlayerToLobby(player);
} else {
sender.sendMessage("Only players may use htis command.");
}
diff --git a/src/main/java/gg/malloc/defense/config/Configuration.java b/src/main/java/gg/malloc/defense/config/Configuration.java
index 50adbed..aeea8b7 100644
--- a/src/main/java/gg/malloc/defense/config/Configuration.java
+++ b/src/main/java/gg/malloc/defense/config/Configuration.java
@@ -18,6 +18,11 @@ public class Configuration {
m_root = rootConfig;
}
+ public String getLobbyName() {
+ ConfigurationSection lobbyConf = m_root.getConfigurationSection("lobby");
+ return lobbyConf.getString("world");
+ }
+
public Collection getMapNames() {
ConfigurationSection mapList = m_root.getConfigurationSection("maps");
return mapList.getKeys(false);
diff --git a/src/main/java/gg/malloc/defense/engine/GameRunner.java b/src/main/java/gg/malloc/defense/engine/GameRunner.java
index 0aac99a..508a228 100644
--- a/src/main/java/gg/malloc/defense/engine/GameRunner.java
+++ b/src/main/java/gg/malloc/defense/engine/GameRunner.java
@@ -5,8 +5,14 @@ import gg.malloc.defense.model.Game;
import gg.malloc.defense.model.Spawner;
import gg.malloc.defense.model.Waypoint;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+
import gg.malloc.defense.ui.BombCarrier;
import gg.malloc.defense.ui.BossBars;
+import gg.malloc.defense.ui.Items;
import gg.malloc.defense.Plugin;
@@ -31,6 +37,7 @@ import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
public class GameRunner {
Arena m_arena;
@@ -56,6 +63,7 @@ public class GameRunner {
TickTask m_fuseTask;
TickTask m_countdownTask;
+ TickTask m_lobbyReturnTask;
TickTask m_bombSmokeTask;
TickTask m_bombCrackleTask;
@@ -91,14 +99,36 @@ public class GameRunner {
if (m_warmupCountdown == 0) {
requestTransition(Stage.Playing);
} else {
- m_bars.update();
- broadcastMessage("Starting game in " + m_warmupCountdown);
+ BaseComponent[] message = new ComponentBuilder()
+ .append("Starting game in ").color(ChatColor.LIGHT_PURPLE)
+ .append("" + m_warmupCountdown).color(ChatColor.AQUA).bold(true)
+ .create();
+ broadcastMessage(message);
m_warmupCountdown--;
m_bars.setCountdownProgress((double)m_warmupCountdown / 10.0);
m_bars.update();
}
}, 20);
+ m_lobbyReturnTask = new TickTask(m_plugin, () -> {
+ if (m_gameEndCountdown == 0) {
+ for(Player p : new ArrayList(m_players.getPlayers())) {
+ removePlayer(p);
+ m_plugin.returnPlayerToLobby(p);
+ }
+ requestTransition(Stage.Idle);
+ } else {
+ BaseComponent[] message = new ComponentBuilder()
+ .append("Game is ending in ").color(ChatColor.LIGHT_PURPLE)
+ .append("" + m_gameEndCountdown).color(ChatColor.RED).bold(true)
+ .create();
+ broadcastMessage(message);
+ m_gameEndCountdown--;
+ m_bars.setCountdownProgress((double)m_gameEndCountdown / 30.0);
+ m_bars.update();
+ }
+ }, 20);
+
m_bombSmokeTask = new TickTask(m_plugin, () -> {
Location targetLoc = getLocation(m_arena.bombTarget());
m_world.spawnParticle(Particle.SMOKE_LARGE, targetLoc, 35, 4, 2, 4);
@@ -111,6 +141,7 @@ public class GameRunner {
}
int m_warmupCountdown = 0;
+ int m_gameEndCountdown = 0;
public void handleEntityRetargeting(EntityTargetEvent evt) {
m_mobs.handleEntityRetarget(evt);
@@ -129,12 +160,23 @@ public class GameRunner {
m_world.strikeLightningEffect(getLocation(m_arena.bombTarget()));
m_world.playSound(getLocation(m_arena.bombTarget()), Sound.ENTITY_GENERIC_EXPLODE, SoundCategory.NEUTRAL, 1.3f, 1.0f);
m_world.spawnParticle(Particle.EXPLOSION_HUGE, getLocation(m_arena.bombTarget()), 8, 5, 2, 5);
+ BaseComponent[] message = new ComponentBuilder()
+ .append("The bomb blew up!").color(ChatColor.RED).bold(true)
+ .create();
+ broadcastMessage(message);
requestTransition(Stage.GameOver);
m_bombSmokeTask.start();
m_bombCrackleTask.start();
}
} else if (evt.getEntity() instanceof Player) {
Player player = (Player)evt.getEntity();
+ if (evt instanceof EntityDamageByEntityEvent) {
+ EntityDamageByEntityEvent entityEvt = (EntityDamageByEntityEvent)evt;
+ if (entityEvt.getDamager() instanceof Player) {
+ evt.setCancelled(true);
+ return;
+ }
+ }
if (m_players.contains(player) && player.getHealth() - evt.getFinalDamage() <= 0) {
evt.setCancelled(true);
handlePlayerDeath(player);
@@ -151,6 +193,7 @@ public class GameRunner {
m_players.requestTransitionForAll(PlayerManager.State.Idle);
m_fuseTask.stop();
m_countdownTask.stop();
+ m_lobbyReturnTask.stop();
return true;
}
@@ -162,7 +205,13 @@ public class GameRunner {
}
m_players.setReady(p, false);
}
- broadcastTitle("Warmup", "Prepare yourself for wave " + m_waves.currentWaveNum());
+ BaseComponent[] message = new ComponentBuilder()
+ .append("Click").color(ChatColor.LIGHT_PURPLE)
+ .append("[Here]").color(ChatColor.GOLD)
+ .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/malloc-defense:ready"))
+ .append(" when ready.").color(ChatColor.LIGHT_PURPLE)
+ .create();
+ broadcastMessage(message);
m_mobs.clear();
return true;
}
@@ -171,8 +220,24 @@ public class GameRunner {
setPlayerReady(p, !m_players.isReady(p));
}
+ void clearReadyState() {
+ for(Player p : m_players.getPlayers()) {
+ setPlayerReady(p, false);
+ }
+ }
+
public void setPlayerReady(Player p, boolean isReady) {
- m_players.setReady(p, isReady);
+ if (isReady != m_players.isReady(p)) {
+ if (isReady) {
+ BaseComponent[] message = new ComponentBuilder()
+ .append(p.getName()).bold(true)
+ .append(" is ready!").color(ChatColor.AQUA).italic(true)
+ .create();
+ }
+ m_players.setReady(p, isReady);
+ }
+ m_bars.update();
+ broadcastMessage(message);
if (m_players.isEveryoneReady()) {
requestTransition(Stage.Countdown);
}
@@ -197,19 +262,15 @@ public class GameRunner {
m_countdownTask.stop();
m_fuseTask.start();
spawnNextBatch();
- broadcastTitle("Wave " + m_waves.currentWaveNum());
return true;
}
private boolean enterGameOver() {
- broadcastTitle("Game Over!");
- broadcastMessage("The bomb blew up!");
+ m_gameEndCountdown = 60;
+ m_lobbyReturnTask.start();
m_countdownTask.stop();
m_fuseTask.stop();
m_mobs.clear();
- for(Player p : new ArrayList(m_players.getPlayers())) {
- removePlayer(p);
- }
return true;
}
@@ -218,7 +279,7 @@ public class GameRunner {
}
private void spawnNextBatch() {
- broadcastMessage("Spawning batch " + m_waves.currentBatchNum());
+ m_log.info("Spawning batch " + m_waves.currentBatchNum());
Spawner spawner = new GameSpawner(m_world, m_arena, m_mobs, m_players);
m_waves.currentWave().spawnBatch(spawner, m_waves.currentBatchNum());
m_bars.update();
@@ -228,7 +289,12 @@ public class GameRunner {
if (m_players.requestTransition(player, PlayerManager.State.Dead)) {
m_world.strikeLightningEffect(player.getLocation());
if (!m_players.isAnyoneAlive()) {
- broadcastMessage("Everyone is dead :(");
+ BaseComponent[] message = new ComponentBuilder()
+ .append("Everyone is ").color(ChatColor.LIGHT_PURPLE)
+ .append("DEAD").color(ChatColor.RED).bold(true)
+ .append(" :(").color(ChatColor.LIGHT_PURPLE)
+ .create();
+ broadcastMessage(message);
requestTransition(Stage.GameOver);
} else {
m_log.info("Remaining players " + m_players.remainingPlayers());
@@ -239,11 +305,23 @@ public class GameRunner {
public void handleEntityDeath(Entity entity) {
boolean wasCarrier = m_mobs.isBombCarrier(entity);
if (m_mobs.killMob(entity)) {
+ int coinsToDrop = 300;
+ while(coinsToDrop > 0) {
+ int droppedCoins = Math.min(coinsToDrop, 64);
+ ItemStack coins = Items.makeCoins();
+ coins.setAmount(droppedCoins);
+ coinsToDrop -= 64;
+ m_world.dropItem(entity.getLocation(), coins);
+ }
m_bars.update();
if (m_mobs.remainingMobs() <= 3) {
m_log.info("Starting next batch!");
if (m_waves.isLastBatch()) {
if (m_waves.isLastWave()) {
+ BaseComponent[] message = new ComponentBuilder()
+ .append("You Won!").color(ChatColor.LIGHT_PURPLE).bold(true)
+ .create();
+ broadcastMessage(message);
requestTransition(Stage.GameOver);
} else if (m_mobs.empty()) {
requestTransition(Stage.Warmup);
@@ -262,7 +340,6 @@ public class GameRunner {
if (m_stage == stage) {
return false;
}
- m_log.info("Game state: " + stage);
switch(stage) {
case Idle:
return enterIdle();
@@ -303,12 +380,28 @@ public class GameRunner {
m_log.info("Game transition: " + m_stage + " -> " + stage);
m_stage = stage;
m_bars.update();
+ for(Player p : m_players.getPlayers()) {
+ sendStageTitle(p);
+ }
return true;
}
m_log.severe("Failed to complete transition: " + m_stage + " -> " + stage);
return false;
}
+ void sendStageTitle(Player p) {
+ switch(m_stage) {
+ case Warmup:
+ p.sendTitle("Warmup", "Prepare yourself for wave " + m_waves.currentWaveNum(), 10, 70, 20);
+ break;
+ case Playing:
+ p.sendTitle("Wave " + m_waves.currentWaveNum(), "", 10, 70, 20);
+ break;
+ case GameOver:
+ p.sendTitle("Game over!", "", 10, 70, 20);
+ }
+ }
+
public void addPlayer(Player p) {
if (m_players.contains(p)) {
return;
@@ -319,7 +412,12 @@ public class GameRunner {
if (m_players.requestTransition(p, PlayerManager.State.Playing)) {
p.teleport(m_world.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
}
- broadcastMessage(p.getName() + " has joined the game");
+ BaseComponent[] message = new ComponentBuilder()
+ .append(p.getName()).bold(true)
+ .append("Has joined the game").color(ChatColor.AQUA)
+ .create();
+ broadcastMessage(message);
+ sendStageTitle(p);
if (m_stage == Stage.Idle) {
requestTransition(Stage.Warmup);
}
@@ -331,6 +429,12 @@ public class GameRunner {
m_players.removePlayer(p);
if (m_players.isEmpty()) {
requestTransition(Stage.Idle);
+ } else {
+ BaseComponent[] message = new ComponentBuilder()
+ .append(p.getName()).bold(true)
+ .append("Has left the game").color(ChatColor.AQUA)
+ .create();
+ broadcastMessage(message);
}
}
@@ -344,9 +448,9 @@ public class GameRunner {
}
}
- void broadcastMessage(String string) {
+ void broadcastMessage(BaseComponent[] component) {
for(Player p : m_world.getPlayers()) {
- p.sendMessage(string);
+ p.spigot().sendMessage(component);
}
}
}
diff --git a/src/main/java/gg/malloc/defense/engine/PlayerManager.java b/src/main/java/gg/malloc/defense/engine/PlayerManager.java
index 37058ba..35bd426 100644
--- a/src/main/java/gg/malloc/defense/engine/PlayerManager.java
+++ b/src/main/java/gg/malloc/defense/engine/PlayerManager.java
@@ -62,7 +62,7 @@ public class PlayerManager {
//m_log.fine("Respawning player " + player);
player.setGameMode(Bukkit.getDefaultGameMode());
player.setHealth(player.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue());
- player.setFoodLevel(10);
+ player.setFoodLevel(20);
return true;
}
@@ -109,6 +109,7 @@ public class PlayerManager {
//m_log.info("Removing player " + player);
requestTransition(player, State.Idle);
m_playerStates.remove(player);
+ m_playerReadyStates.remove(player);
return true;
}
diff --git a/src/main/java/gg/malloc/defense/ui/BombCarrier.java b/src/main/java/gg/malloc/defense/ui/BombCarrier.java
index 5b9dbe7..822177e 100644
--- a/src/main/java/gg/malloc/defense/ui/BombCarrier.java
+++ b/src/main/java/gg/malloc/defense/ui/BombCarrier.java
@@ -15,23 +15,15 @@ public class BombCarrier {
public LivingEntity inHand() {
EntityEquipment equipment = m_entity.getEquipment();
- equipment.setItemInOffHand(makeBombHelmet());
+ equipment.setItemInOffHand(Items.makeBombHelmet());
//equipment.setItemInOffHandDropChance(0.0f);
return m_entity;
}
public LivingEntity onHead() {
EntityEquipment equipment = m_entity.getEquipment();
- equipment.setHelmet(makeBombHelmet());
+ equipment.setHelmet(Items.makeBombHelmet());
//equipment.setHelmetDropChance(0.0f);
return m_entity;
}
-
- static ItemStack makeBombHelmet() {
- ItemStack bombItem = new ItemStack(Material.CARVED_PUMPKIN);
- ItemMeta meta = bombItem.getItemMeta();
- meta.setCustomModelData(33197);
- bombItem.setItemMeta(meta);
- return bombItem;
- }
}
diff --git a/src/main/java/gg/malloc/defense/ui/BossBars.java b/src/main/java/gg/malloc/defense/ui/BossBars.java
index f4c3642..1306009 100644
--- a/src/main/java/gg/malloc/defense/ui/BossBars.java
+++ b/src/main/java/gg/malloc/defense/ui/BossBars.java
@@ -108,7 +108,10 @@ public class BossBars {
m_gameBar.setColor(BarColor.RED);
m_gameBar.setProgress(1.0);
m_gameBar.setTitle("Game Over!");
- m_waveBar.setVisible(false);
+ m_waveBar.setVisible(true);
+ m_waveBar.setColor(BarColor.BLUE);
+ m_waveBar.setTitle("Returning to lobby...");
+ m_waveBar.setProgress(m_countdownProgress);
m_bombBar.setVisible(false);
break;
}
diff --git a/src/main/java/gg/malloc/defense/ui/Items.java b/src/main/java/gg/malloc/defense/ui/Items.java
index 3de3af4..c169093 100644
--- a/src/main/java/gg/malloc/defense/ui/Items.java
+++ b/src/main/java/gg/malloc/defense/ui/Items.java
@@ -1,14 +1,29 @@
-package gg.malloc.defense.engine;
+package gg.malloc.defense.ui;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.Material;
+import de.tr7zw.nbtapi.NBTItem;
+import de.tr7zw.nbtapi.NBTCompound;
+
public class Items {
- static ItemStack makeBombHelmet() {
+ public static ItemStack makeCoins() {
+ ItemStack coinItem = new ItemStack(Material.IRON_NUGGET);
+ ItemMeta meta = coinItem.getItemMeta();
+ meta.setCustomModelData(93197);
+ coinItem.setItemMeta(meta);
+
+ NBTItem nbt = new NBTItem(coinItem);
+ nbt.addCompound("malloc").setInteger("coinValue", 1);
+
+ return nbt.getItem();
+ }
+
+ public static ItemStack makeBombHelmet() {
ItemStack bombItem = new ItemStack(Material.CARVED_PUMPKIN);
ItemMeta meta = bombItem.getItemMeta();
- meta.setCustomModelData(0);
+ meta.setCustomModelData(33197);
bombItem.setItemMeta(meta);
return bombItem;
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 5acdce0..b6178eb 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,3 +1,5 @@
+lobby:
+ world: world
maps:
quarry:
target: