Implement /ready and basic objective mechanics

This commit is contained in:
Torrie Fischer 2022-05-08 18:36:35 +02:00
parent a078f74b4b
commit 2f63695ee9
11 changed files with 263 additions and 36 deletions

86
TODO.md
View File

@ -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

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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<Arena> 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());
}
}

View File

@ -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;
}
}
}

View File

@ -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,9 +123,31 @@ 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) {
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() {
if (m_warmupCountdown == 0) {
@ -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,20 +180,49 @@ 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;
@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -11,6 +11,7 @@ import java.util.Collection;
public class PlayerManager {
HashMap<Player, State> m_playerStates = new HashMap<>();
HashMap<Player, Boolean> 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) {

View File

@ -6,4 +6,5 @@ public interface Arena {
World getWorld();
String name();
Spawnpoint[] spawnpoints();
Spawnpoint bombTarget();
}

View File

@ -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