Reimplement wave AI, split out some UI code into a ui module, update TODO

This commit is contained in:
Torrie Fischer 2022-05-10 05:28:01 +02:00
parent 2f63695ee9
commit 75debe1905
15 changed files with 588 additions and 248 deletions

24
TODO.md
View File

@ -2,19 +2,33 @@
[X] Mobs spawn in waves [X] Mobs spawn in waves
[X] Mobs move towards goal [X] Mobs move towards goal
[ ] Mobs carry bomb to goal [X] Mobs carry bomb to goal
[X] Mobs arm bomb [X] Mobs arm bomb
[X] Bomb explodes [X] Bomb explodes
# Malloc Beta
[ ] Join games
[ ] Leave games
[ ] One arena config
# Scaled waves
[ ] Weaker mobs, more of them
[ ] Mob categories
[ ] Spawnpoint categories
# UX # UX
[X] Wave boss bar [X] Wave boss bar
[X] Mob count boss bar [X] Mob count boss bar
[X] Stage titles [X] Stage titles
[ ] EXPLOSIONS [X] EXPLOSIONS
[ ] Target catches on fire more it is lit
[ ] Colored titles [ ] Colored titles
[ ] Clickable /ready in chat [ ] Clickable /ready in chat
[ ] Sidebar [ ] Sidebar
[ ] List of mobs in next wave
# Social # Social
@ -27,9 +41,9 @@
# Mechanics # Mechanics
[ ] Coin drops [ ] Coin drops
[ ] Mob tracking should prioritize bomb [X] Mob tracking should prioritize bomb
[ ] Mobs recover dropped bombs [X] Mobs recover dropped bombs
[ ] Bomb carriers are slower [X] Bomb carriers are slower
[ ] Bonus coins for complete coin pickup [ ] Bonus coins for complete coin pickup
[ ] Infinite weapons + armor [ ] Infinite weapons + armor
[ ] Ammo/health spawns [ ] Ammo/health spawns

View File

@ -40,14 +40,6 @@ public class GameEventHandler implements Listener {
@EventHandler @EventHandler
public void onEntityDamage(EntityDamageEvent evt) { public void onEntityDamage(EntityDamageEvent evt) {
if (evt.getEntity() instanceof Player) {
Player player = (Player)evt.getEntity();
if (player.getHealth() - evt.getFinalDamage() <= 0) {
evt.setCancelled(true);
m_runner.handlePlayerDeath(player);
}
} else {
m_runner.handleEntityDamage(evt); m_runner.handleEntityDamage(evt);
} }
}
} }

View File

@ -22,21 +22,24 @@ public class SetStageCommand implements CommandExecutor {
GameRunner runner = m_plugin.getRunnerForWorld(world); GameRunner runner = m_plugin.getRunnerForWorld(world);
String stateName = args[0].toLowerCase(); String stateName = args[0].toLowerCase();
boolean ret = false; boolean ret = false;
if (stateName.equals("idle")) { GameRunner.Stage decodedStage = null;
ret = runner.requestTransition(GameRunner.Stage.Idle); GameRunner.Stage stages[] = GameRunner.Stage.Idle.getDeclaringClass().getEnumConstants();
} else if (stateName.equals("warmup")) { for(GameRunner.Stage stage : stages) {
ret = runner.requestTransition(GameRunner.Stage.Warmup); if (stage.toString().toLowerCase().equals(stateName)) {
} else if (stateName.equals("playing")) { decodedStage = stage;
ret = runner.requestTransition(GameRunner.Stage.Playing); break;
} else if (stateName.equals("gameover")) { }
ret = runner.requestTransition(GameRunner.Stage.GameOver); }
if (decodedStage != null) {
sender.sendMessage("Requesting transition to " + decodedStage);
if (!runner.requestTransition(decodedStage)) {
sender.sendMessage("Could not transition to " + decodedStage);
return false;
}
return true;
} else { } else {
sender.sendMessage("Unknown state " + stateName); sender.sendMessage("Unknown state " + stateName);
return false; return false;
} }
if (!ret) {
sender.sendMessage("Could not set state to " + stateName);
}
return ret;
} }
} }

View File

@ -0,0 +1,30 @@
package gg.malloc.defense.engine;
public class BombFuse {
int m_bombFuseCount = 0;
int m_bombFuseTarget = 10;
public double getProgress() {
return Math.max(0.0, Math.min(1.0, (double)m_bombFuseCount / (double)m_bombFuseTarget));
}
public boolean isLit() {
return m_bombFuseCount > 0;
}
public boolean isExploded() {
return m_bombFuseCount > m_bombFuseTarget;
}
public void reset() {
m_bombFuseCount = 0;
}
public void tickLit() {
m_bombFuseCount += 1;
}
public void tickDecay() {
m_bombFuseCount -= 1;
}
}

View File

@ -1,30 +1,35 @@
package gg.malloc.defense.engine; package gg.malloc.defense.engine;
import gg.malloc.defense.model.Game;
import gg.malloc.defense.model.Arena; import gg.malloc.defense.model.Arena;
import gg.malloc.defense.model.Game;
import gg.malloc.defense.model.Spawner; import gg.malloc.defense.model.Spawner;
import gg.malloc.defense.model.Wave;
import gg.malloc.defense.ui.BombCarrier;
import gg.malloc.defense.ui.BossBars;
import gg.malloc.defense.Plugin; import gg.malloc.defense.Plugin;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.ArrayList;
import java.util.HashSet;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.World; 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.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityTargetEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
public class GameRunner { public class GameRunner {
Arena m_arena; Arena m_arena;
@ -32,18 +37,11 @@ public class GameRunner {
Stage m_stage; Stage m_stage;
Plugin m_plugin; 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);
BossBar m_bombBar = Bukkit.createBossBar("Bomb HP", BarColor.RED, BarStyle.SOLID);
BukkitTask m_countdownTask;
MobManager m_mobs; MobManager m_mobs;
WaveManager m_waves; WaveManager m_waves;
PlayerManager m_players; PlayerManager m_players;
LivingEntity m_bombTarget = null; BombFuse m_bombFuse;
int m_bombHP = 100;
Logger m_log; Logger m_log;
@ -55,126 +53,96 @@ public class GameRunner {
GameOver GameOver
} }
class WaveManager { TickTask m_fuseTask;
int m_currentWaveNum = 0; TickTask m_countdownTask;
int m_currentBatch = 0; TickTask m_bombSmokeTask;
Wave m_currentWave = null; TickTask m_bombCrackleTask;
Game m_game;
WaveManager(Game game) {
m_game = game;
}
void reset() {
m_currentWaveNum = 0;
m_currentBatch = 0;
m_currentWave = null;
}
Wave currentWave() {
return m_currentWave;
}
int currentWaveNum() {
return m_currentWaveNum;
}
int currentBatchNum() {
return m_currentBatch;
}
double progress() {
return (double)m_currentWaveNum / (double)m_game.getWaveCount();
}
boolean isLastWave() {
return m_currentWaveNum >= m_game.getWaveCount();
}
boolean isLastBatch() {
return m_currentBatch >= m_currentWave.batchCount();
}
void nextBatch() {
m_currentBatch += 1;
}
void next() {
m_currentWaveNum += 1;
m_currentBatch = 0;
m_currentWave = m_game.getWave(m_currentWaveNum);
}
}
BossBars m_bars;
public GameRunner(Plugin plugin, Game game, Arena arena) { public GameRunner(Plugin plugin, Game game, Arena arena) {
m_plugin = plugin; m_plugin = plugin;
m_game = game; m_game = game;
m_arena = arena; m_arena = arena;
m_stage = Stage.Idle; m_stage = Stage.Idle;
m_gameBar.setVisible(true); m_mobs = new MobManager();
m_waveBar.setVisible(false);
m_bombBar.setVisible(false);
m_mobs = new MobManager(m_game);
m_waves = new WaveManager(m_game); m_waves = new WaveManager(m_game);
m_players = new PlayerManager(); m_players = new PlayerManager();
m_bombFuse = new BombFuse();
m_bars = new BossBars(this, m_game, m_waves, m_players, m_bombFuse, m_mobs);
m_log = m_plugin.getLogger(); m_log = m_plugin.getLogger();
m_fuseTask = new TickTask(m_plugin, () -> {
if (m_bombFuse.isLit()) {
m_bombFuse.tickDecay();
m_bars.update();
}
}, 80);
m_countdownTask = new TickTask(m_plugin, () -> {
if (m_warmupCountdown == 0) {
requestTransition(Stage.Playing);
} else {
m_bars.update();
broadcastMessage("Starting game in " + m_warmupCountdown);
m_warmupCountdown--;
m_bars.setCountdownProgress((double)m_warmupCountdown / 10.0);
m_bars.update();
}
}, 20);
m_bombSmokeTask = new TickTask(m_plugin, () -> {
Location targetLoc = m_arena.bombTarget().getLocation();
m_arena.getWorld().spawnParticle(Particle.SMOKE_LARGE, targetLoc, 35, 4, 2, 4);
m_arena.getWorld().spawnParticle(Particle.SMALL_FLAME, targetLoc, 30, 3, 2, 3);
}, 5);
m_bombCrackleTask = new TickTask(m_plugin, () -> {
Location targetLoc = m_arena.bombTarget().getLocation();
m_arena.getWorld().playSound(targetLoc, Sound.BLOCK_CAMPFIRE_CRACKLE, SoundCategory.NEUTRAL, 1.0f, 1.0f);
}, 35);
} }
int m_warmupCountdown = 0; int m_warmupCountdown = 0;
public void handleEntityRetargeting(EntityTargetEvent evt) { public void handleEntityRetargeting(EntityTargetEvent evt) {
if (m_mobs.contains(evt.getEntity()) && evt.getReason() == EntityTargetEvent.TargetReason.FORGOT_TARGET) { m_mobs.handleEntityRetarget(evt);
evt.setTarget(m_bombTarget);
}
} }
public void handleEntityDamage(EntityDamageEvent evt) { 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); m_mobs.handleEntityDamage(evt);
if (m_mobs.bombWasHit()) {
EntityDamageByEntityEvent entityEvt = (EntityDamageByEntityEvent)evt;
m_log.info("Target attacked!");
entityEvt.getDamager().setGlowing(true);
m_bombFuse.tickLit();
m_arena.getWorld().playSound(m_arena.bombTarget().getLocation(), Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR, 1.5f, 0.9f);
m_bars.update();
if (m_bombFuse.isExploded()) {
m_arena.getWorld().strikeLightningEffect(m_arena.bombTarget().getLocation());
m_arena.getWorld().playSound(m_arena.bombTarget().getLocation(), Sound.ENTITY_GENERIC_EXPLODE, SoundCategory.NEUTRAL, 1.3f, 1.0f);
m_arena.getWorld().spawnParticle(Particle.EXPLOSION_HUGE, m_arena.bombTarget().getLocation(), 8, 5, 2, 5);
requestTransition(Stage.GameOver);
m_bombSmokeTask.start();
m_bombCrackleTask.start();
} }
} else if (evt.getEntity() instanceof Player) {
Player player = (Player)evt.getEntity();
if (m_players.contains(player) && player.getHealth() - evt.getFinalDamage() <= 0) {
evt.setCancelled(true);
handlePlayerDeath(player);
} }
private void countdownTick() {
if (m_warmupCountdown == 0) {
requestTransition(Stage.Playing);
} else {
updateMobBars();
broadcastMessage("Starting game in " + m_warmupCountdown);
m_warmupCountdown--;
m_countdownTask = m_plugin.getServer().getScheduler().runTaskLater(m_plugin, () -> {
countdownTick();
}, 20);
} }
} }
private boolean enterIdle() { private boolean enterIdle() {
m_bombHP = 100; m_bombFuse.reset();
m_waves.reset(); m_waves.reset();
m_mobs.clear(); m_mobs.clear();
m_bombSmokeTask.stop();
m_bombCrackleTask.stop();
m_players.requestTransitionForAll(PlayerManager.State.Idle); m_players.requestTransitionForAll(PlayerManager.State.Idle);
if (m_bombTarget != null) { m_fuseTask.stop();
m_bombTarget.remove(); m_countdownTask.stop();
m_bombTarget = null;
}
if (m_countdownTask != null) {
m_countdownTask.cancel();
m_countdownTask = null;
}
return true; return true;
} }
@ -204,25 +172,22 @@ public class GameRunner {
private boolean enterCountdown() { private boolean enterCountdown() {
m_warmupCountdown = 10; m_warmupCountdown = 10;
countdownTick(); m_bars.setCountdownProgress(1.0);
m_countdownTask.start();
return true; return true;
} }
private boolean enterPlaying() { private boolean enterPlaying() {
m_log.info("Starting wave " + m_waves.currentWaveNum()); m_log.info("Starting wave " + m_waves.currentWaveNum());
if (m_bombTarget == null) { m_mobs.spawnTarget(m_arena.bombTarget().getLocation());
m_bombTarget = (LivingEntity)m_arena.getWorld().spawnEntity(m_arena.bombTarget().getLocation(), EntityType.ARMOR_STAND); // TODO: Set helmet with custom model data
}
ArmorStand bombStand = (ArmorStand)m_bombTarget;
bombStand.setCustomName("Bomb Target");
bombStand.setVisible(true);
bombStand.setCustomNameVisible(true);
bombStand.setGlowing(true);
for(Player p : m_players.getPlayers()) { for(Player p : m_players.getPlayers()) {
if (m_players.requestTransition(p, PlayerManager.State.Playing)) { if (m_players.requestTransition(p, PlayerManager.State.Playing)) {
p.teleport(m_arena.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); p.teleport(m_arena.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
} }
} }
m_countdownTask.stop();
m_fuseTask.start();
spawnNextBatch(); spawnNextBatch();
broadcastTitle("Wave " + m_waves.currentWaveNum()); broadcastTitle("Wave " + m_waves.currentWaveNum());
return true; return true;
@ -230,80 +195,28 @@ public class GameRunner {
private boolean enterGameOver() { private boolean enterGameOver() {
broadcastTitle("Game Over!"); broadcastTitle("Game Over!");
if (m_countdownTask != null) { broadcastMessage("The bomb blew up!");
m_countdownTask.cancel(); m_countdownTask.stop();
m_countdownTask = null; m_fuseTask.stop();
}
m_mobs.clear(); m_mobs.clear();
for(Player p : m_players.getPlayers()) { for(Player p : new ArrayList<Player>(m_players.getPlayers())) {
removePlayer(p); removePlayer(p);
} }
return true; return true;
} }
private void updateMobBars() { public Stage getStage() {
m_gameBar.setVisible(true); return m_stage;
switch(m_stage) {
case Idle:
m_gameBar.setProgress(1.0);
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("Wave starting!");
m_waveBar.setProgress((double)m_warmupCountdown / (double)10);
m_bombBar.setVisible(false);
break;
case Playing:
m_gameBar.setProgress(m_waves.progress());
m_gameBar.setTitle("Wave " + m_waves.currentWaveNum() + " / " + m_game.getWaveCount());
m_gameBar.setColor(BarColor.PURPLE);
if (m_mobs.createdMobs() > 0) {
m_waveBar.setVisible(true);
m_waveBar.setColor(BarColor.GREEN);
m_waveBar.setTitle("Mobs remaining: " + m_mobs.remainingMobs());
m_waveBar.setProgress(m_mobs.progress());
} 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() { private void spawnNextBatch() {
broadcastMessage("Spawning batch " + m_waves.currentBatchNum()); broadcastMessage("Spawning batch " + m_waves.currentBatchNum());
Spawner spawner = new GameSpawner(m_arena, m_mobs, m_players, m_bombTarget); Spawner spawner = new GameSpawner(m_arena, m_mobs, m_players);
m_waves.currentWave().spawnBatch(spawner, m_waves.currentBatchNum()); m_waves.currentWave().spawnBatch(spawner, m_waves.currentBatchNum());
updateMobBars(); m_bars.update();
} }
public void handlePlayerDeath(Player player) { private void handlePlayerDeath(Player player) {
if (m_players.requestTransition(player, PlayerManager.State.Dead)) { if (m_players.requestTransition(player, PlayerManager.State.Dead)) {
m_arena.getWorld().strikeLightningEffect(player.getLocation()); m_arena.getWorld().strikeLightningEffect(player.getLocation());
if (!m_players.isAnyoneAlive()) { if (!m_players.isAnyoneAlive()) {
@ -316,9 +229,9 @@ public class GameRunner {
} }
public void handleEntityDeath(Entity entity) { public void handleEntityDeath(Entity entity) {
boolean wasCarrier = m_mobs.isBombCarrier(entity);
if (m_mobs.killMob(entity)) { if (m_mobs.killMob(entity)) {
broadcastMessage("Killed game entity " + entity); m_bars.update();
updateMobBars();
if (m_mobs.remainingMobs() <= 3) { if (m_mobs.remainingMobs() <= 3) {
m_log.info("Starting next batch!"); m_log.info("Starting next batch!");
if (m_waves.isLastBatch()) { if (m_waves.isLastBatch()) {
@ -381,7 +294,7 @@ public class GameRunner {
if (attemptTransition(stage)) { if (attemptTransition(stage)) {
m_log.info("Game transition: " + m_stage + " -> " + stage); m_log.info("Game transition: " + m_stage + " -> " + stage);
m_stage = stage; m_stage = stage;
updateMobBars(); m_bars.update();
return true; return true;
} }
m_log.severe("Failed to complete transition: " + m_stage + " -> " + stage); m_log.severe("Failed to complete transition: " + m_stage + " -> " + stage);
@ -389,10 +302,11 @@ public class GameRunner {
} }
public void addPlayer(Player p) { public void addPlayer(Player p) {
if (m_players.contains(p)) {
return;
}
m_players.addPlayer(p); m_players.addPlayer(p);
m_gameBar.addPlayer(p); m_bars.addPlayer(p);
m_waveBar.addPlayer(p);
m_bombBar.addPlayer(p);
if (m_stage == Stage.Idle || m_stage == Stage.Warmup) { if (m_stage == Stage.Idle || m_stage == Stage.Warmup) {
if (m_players.requestTransition(p, PlayerManager.State.Playing)) { if (m_players.requestTransition(p, PlayerManager.State.Playing)) {
p.teleport(m_arena.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN); p.teleport(m_arena.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
@ -405,8 +319,7 @@ public class GameRunner {
} }
public void removePlayer(Player p) { public void removePlayer(Player p) {
m_gameBar.removePlayer(p); m_bars.removePlayer(p);
m_waveBar.removePlayer(p);
m_players.removePlayer(p); m_players.removePlayer(p);
if (m_players.isEmpty()) { if (m_players.isEmpty()) {
requestTransition(Stage.Idle); requestTransition(Stage.Idle);
@ -429,8 +342,4 @@ public class GameRunner {
p.sendMessage(string); p.sendMessage(string);
} }
} }
void registerSpawnedMob(Entity entity) {
m_mobs.addEntity(entity);
}
} }

View File

@ -8,32 +8,44 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity; import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Mob; import org.bukkit.entity.Mob;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
public class GameSpawner implements Spawner { public class GameSpawner implements Spawner {
Arena m_arena; Arena m_arena;
MobManager m_manager; MobManager m_manager;
PlayerManager m_players; PlayerManager m_players;
LivingEntity m_bombTarget;
int m_spawnIdx = 0; int m_spawnIdx = 0;
public GameSpawner(Arena arena, MobManager manager, PlayerManager players, LivingEntity bombTarget) { public GameSpawner(Arena arena, MobManager manager, PlayerManager players) {
m_arena = arena; m_arena = arena;
m_manager = manager; m_manager = manager;
m_players = players; m_players = players;
m_bombTarget = bombTarget;
} }
@Override @Override
public Entity spawnMob(EntityType type) { public LivingEntity spawnBombCarrier(EntityType type) {
if (m_manager.bombCount() == 0) {
return m_manager.addBombCarrier(spawnMob(type));
} else {
return spawnMob(type);
}
}
@Override
public LivingEntity spawnMob(EntityType type) {
Spawnpoint[] spawnpoints = m_arena.spawnpoints(); Spawnpoint[] spawnpoints = m_arena.spawnpoints();
m_spawnIdx %= spawnpoints.length; m_spawnIdx %= spawnpoints.length;
//m_log.fine("Spawning " + type + " at " + spawnpoints[m_spawnIdx]); //m_log.fine("Spawning " + type + " at " + spawnpoints[m_spawnIdx]);
Entity newMob = m_arena.getWorld().spawnEntity(spawnpoints[m_spawnIdx].getLocation(), type); Entity newMob = m_arena.getWorld().spawnEntity(spawnpoints[m_spawnIdx].getLocation(), type);
LivingEntity livingMob = (LivingEntity)newMob; LivingEntity livingMob = (LivingEntity)newMob;
livingMob.setRemoveWhenFarAway(false); livingMob.setRemoveWhenFarAway(false);
m_manager.addEntity(newMob); m_manager.addEntity(livingMob);
m_spawnIdx += 1; m_spawnIdx += 1;
((Mob)newMob).setTarget(m_bombTarget); return livingMob;
return newMob;
} }
} }

View File

@ -2,48 +2,61 @@ package gg.malloc.defense.engine;
import java.util.HashSet; import java.util.HashSet;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityTargetEvent;
import gg.malloc.defense.model.Game; import gg.malloc.defense.model.Game;
import gg.malloc.defense.ui.BombCarrier;
public class MobManager { public class MobManager {
HashSet<Entity> m_livingMobs = new HashSet<>(); HashSet<LivingEntity> m_livingMobs = new HashSet<>();
HashSet<LivingEntity> m_bombCarriers = new HashSet<>();
HashSet<LivingEntity> m_droppedBombs = new HashSet<>();
int m_createdMobs = 0; int m_createdMobs = 0;
int m_killedMobs = 0; int m_killedMobs = 0;
Game m_game;
MobManager(Game game) {
m_game = game;
}
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);
}
}
public boolean contains(Entity entity) { public boolean contains(Entity entity) {
return m_livingMobs.contains(entity); return m_livingMobs.contains(entity);
} }
public boolean isBombCarrier(Entity entity) {
return m_bombCarriers.contains(entity);
}
public boolean killMob(Entity entity) { public boolean killMob(Entity entity) {
if (m_livingMobs.contains(entity)) { if (m_livingMobs.contains(entity)) {
m_killedMobs += 1; m_killedMobs += 1;
if (m_bombCarriers.remove(entity)) {
dropBomb(entity.getLocation());
}
return m_livingMobs.remove(entity); return m_livingMobs.remove(entity);
} }
return false; return false;
} }
public void clear() { public void clear() {
for(Entity e : m_livingMobs) { for(LivingEntity e : m_livingMobs) {
e.remove(); e.remove();
} }
for(LivingEntity e : m_droppedBombs) {
e.remove();
}
if (m_bombTarget != null) {
m_bombTarget.remove();
}
m_bombTarget = null;
m_droppedBombs.clear();
m_livingMobs.clear(); m_livingMobs.clear();
m_bombCarriers.clear();
m_createdMobs = 0; m_createdMobs = 0;
m_killedMobs = 0; m_killedMobs = 0;
} }
@ -64,13 +77,106 @@ public class MobManager {
return (double)m_killedMobs / (double)m_createdMobs; return (double)m_killedMobs / (double)m_createdMobs;
} }
public void addEntity(Entity entity) { public void addEntity(LivingEntity entity) {
//m_log.fine("Registered new mob " + entity);
m_livingMobs.add(entity); m_livingMobs.add(entity);
Mob asMob = (Mob)entity;
if (m_droppedBombs.size() > 0) {
asMob.setTarget(m_droppedBombs.iterator().next());
} else {
asMob.setTarget(m_bombTarget);
}
m_createdMobs += 1; m_createdMobs += 1;
} }
public int bombCount() {
return m_bombCarriers.size() + m_droppedBombs.size();
}
public LivingEntity addBombCarrier(LivingEntity entity) {
m_bombCarriers.add(entity);
((Mob)entity).setTarget(m_bombTarget);
return new BombCarrier(entity).inHand();
}
public void removeBombCarrier(LivingEntity entity) {
m_bombCarriers.remove(entity);
}
public boolean empty() { public boolean empty() {
return m_livingMobs.size() == 0; return m_livingMobs.size() == 0;
} }
public LivingEntity dropBomb(Location location) {
ArmorStand droppedBomb = (ArmorStand)(new BombCarrier((LivingEntity)location.getWorld().spawnEntity(location, EntityType.ARMOR_STAND)).onHead());
droppedBomb.setCustomName("The Bomb");
droppedBomb.setVisible(true);
droppedBomb.setCustomNameVisible(true);
droppedBomb.setGlowing(true);
m_droppedBombs.add(droppedBomb);
for(LivingEntity ent : m_livingMobs) {
if (!m_bombCarriers.contains(ent)) {
((Mob)ent).setTarget(droppedBomb);
}
}
return droppedBomb;
}
boolean m_bombWasHit = false;
public boolean bombWasHit() {
return m_bombWasHit;
}
public void handleEntityDamage(EntityDamageEvent evt) {
Entity entity = evt.getEntity();
m_bombWasHit = false;
if (entity == m_bombTarget && evt instanceof EntityDamageByEntityEvent) {
EntityDamageByEntityEvent entityEvt = (EntityDamageByEntityEvent)evt;
evt.setCancelled(true);
if (m_bombCarriers.contains(entityEvt.getDamager())) {
m_bombWasHit = true;
}
} if (m_droppedBombs.contains(entity)) {
evt.setCancelled(true);
EntityDamageByEntityEvent entityEvt = (EntityDamageByEntityEvent)evt;
if (m_livingMobs.contains(entityEvt.getDamager())) {
evt.getEntity().remove();
m_droppedBombs.remove(evt.getEntity());
addBombCarrier((LivingEntity)entityEvt.getDamager());
for(LivingEntity ent : m_livingMobs) {
if (!m_bombCarriers.contains(ent)) {
((Mob)ent).setTarget(m_bombTarget);
}
}
}
} else if (m_livingMobs.contains(evt.getEntity())) {
//m_game.onMobDamaged(evt.getEntity());
}
}
LivingEntity m_bombTarget;
public LivingEntity spawnTarget(Location location) {
assert(m_bombTarget == null);
m_bombTarget = (LivingEntity)location.getWorld().spawnEntity(location, EntityType.ARMOR_STAND);
ArmorStand bombStand = (ArmorStand)m_bombTarget;
bombStand.setCustomName("Bomb Target");
bombStand.setVisible(true);
bombStand.setCustomNameVisible(true);
bombStand.setGlowing(true);
return m_bombTarget;
}
public void handleEntityRetarget(EntityTargetEvent evt) {
if (m_livingMobs.contains(evt.getEntity())) {
if (m_bombCarriers.contains(evt.getEntity())) {
evt.setTarget(m_bombTarget);
} if (m_droppedBombs.size() > 0) {
evt.setTarget(m_droppedBombs.iterator().next());
} else if (evt.getReason() == EntityTargetEvent.TargetReason.FORGOT_TARGET) {
evt.setTarget(m_bombTarget);
}
}
}
} }

View File

@ -27,6 +27,10 @@ public class PlayerManager {
return allSuccess; return allSuccess;
} }
public boolean contains(Player player) {
return m_playerStates.containsKey(player);
}
public boolean requestTransition(Player player, State toState) { public boolean requestTransition(Player player, State toState) {
State currentState = m_playerStates.get(player); State currentState = m_playerStates.get(player);
if (currentState == toState) { if (currentState == toState) {

View File

@ -0,0 +1,40 @@
package gg.malloc.defense.engine;
import gg.malloc.defense.Plugin;
import org.bukkit.scheduler.BukkitTask;
public class TickTask {
Runnable m_runnable;
int m_interval;
BukkitTask m_task = null;
Plugin m_plugin;
boolean m_cancelled = false;
public TickTask(Plugin plugin, Runnable runnable, int interval) {
m_plugin = plugin;
m_runnable = runnable;
m_interval = interval;
}
public void start() {
m_cancelled = false;
if (m_task == null) {
tick();
}
}
private void tick() {
m_runnable.run();
if (!m_cancelled) {
m_task = m_plugin.getServer().getScheduler().runTaskLater(m_plugin, () -> {tick();}, m_interval);
}
}
void stop() {
m_cancelled = true;
if (m_task != null) {
m_task.cancel();
m_task = null;
}
}
}

View File

@ -0,0 +1,55 @@
package gg.malloc.defense.engine;
import gg.malloc.defense.model.Wave;
import gg.malloc.defense.model.Game;
public class WaveManager {
int m_currentWaveNum = 0;
int m_currentBatch = 0;
Wave m_currentWave = null;
Game m_game;
public WaveManager(Game game) {
m_game = game;
}
public void reset() {
m_currentWaveNum = 0;
m_currentBatch = 0;
m_currentWave = null;
}
public Wave currentWave() {
return m_currentWave;
}
public int currentWaveNum() {
return m_currentWaveNum;
}
public int currentBatchNum() {
return m_currentBatch;
}
public double progress() {
return (double)m_currentWaveNum / (double)m_game.getWaveCount();
}
public boolean isLastWave() {
return m_currentWaveNum >= m_game.getWaveCount();
}
public boolean isLastBatch() {
return m_currentBatch >= m_currentWave.batchCount();
}
public void nextBatch() {
m_currentBatch += 1;
}
public void next() {
m_currentWaveNum += 1;
m_currentBatch = 0;
m_currentWave = m_game.getWave(m_currentWaveNum);
}
}

View File

@ -6,6 +6,9 @@ import gg.malloc.defense.model.Spawner;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Mob;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.attribute.Attribute;
import java.util.HashMap; import java.util.HashMap;
@ -55,6 +58,8 @@ public class ScaledWaves implements Game {
@Override @Override
public void spawnBatch(Spawner spawner, int batch) { public void spawnBatch(Spawner spawner, int batch) {
double healthScale = -0.5;
AttributeModifier modifier = new AttributeModifier("Scaled Health", healthScale, AttributeModifier.Operation.MULTIPLY_SCALAR_1);
assert(m_mobsPerBatch > 0); assert(m_mobsPerBatch > 0);
for(int i = 0; i < m_mobsPerBatch; i++) { for(int i = 0; i < m_mobsPerBatch; i++) {
EntityType selectedType = null; EntityType selectedType = null;
@ -67,8 +72,9 @@ public class ScaledWaves implements Game {
} }
} }
assert(selectedType != null); assert(selectedType != null);
Entity newMob = spawner.spawnMob(selectedType); Mob newMob = (Mob)spawner.spawnBombCarrier(selectedType);
newMob.setCustomName("Mob " + i + "/" + m_mobsPerBatch); newMob.setCustomName("Mob " + i + "/" + m_mobsPerBatch);
newMob.getAttribute(Attribute.GENERIC_MAX_HEALTH).addModifier(modifier);
} }
if (m_hasRavager) { if (m_hasRavager) {
Entity newMob = spawner.spawnMob(EntityType.RAVAGER); Entity newMob = spawner.spawnMob(EntityType.RAVAGER);

View File

@ -1,8 +1,9 @@
package gg.malloc.defense.model; package gg.malloc.defense.model;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity;
public interface Spawner { public interface Spawner {
Entity spawnMob(EntityType type); LivingEntity spawnMob(EntityType type);
LivingEntity spawnBombCarrier(EntityType type);
} }

View File

@ -0,0 +1,37 @@
package gg.malloc.defense.ui;
import org.bukkit.entity.LivingEntity;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.Material;
public class BombCarrier {
LivingEntity m_entity;
public BombCarrier(LivingEntity entity) {
m_entity = entity;
}
public LivingEntity inHand() {
EntityEquipment equipment = m_entity.getEquipment();
equipment.setItemInOffHand(makeBombHelmet());
//equipment.setItemInOffHandDropChance(0.0f);
return m_entity;
}
public LivingEntity onHead() {
EntityEquipment equipment = m_entity.getEquipment();
equipment.setHelmet(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;
}
}

View File

@ -0,0 +1,116 @@
package gg.malloc.defense.ui;
import gg.malloc.defense.engine.GameRunner;
import gg.malloc.defense.engine.WaveManager;
import gg.malloc.defense.engine.PlayerManager;
import gg.malloc.defense.engine.MobManager;
import gg.malloc.defense.engine.BombFuse;
import gg.malloc.defense.model.Game;
import org.bukkit.boss.BossBar;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BarColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
public class BossBars {
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 Fuse", BarColor.RED, BarStyle.SOLID);
GameRunner m_runner;
MobManager m_mobs;
WaveManager m_waves;
PlayerManager m_players;
BombFuse m_fuse;
Game m_game;
double m_countdownProgress;
public void setCountdownProgress(double v) {
m_countdownProgress = v;
}
public BossBars(GameRunner runner, Game game, WaveManager waves, PlayerManager players, BombFuse fuse, MobManager mobs) {
m_runner = runner;
m_game = game;
m_waves = waves;
m_players = players;
m_fuse = fuse;
m_mobs = mobs;
m_gameBar.setVisible(true);
m_waveBar.setVisible(false);
m_bombBar.setVisible(false);
}
public void addPlayer(Player p) {
m_gameBar.addPlayer(p);
m_waveBar.addPlayer(p);
m_bombBar.addPlayer(p);
}
public void removePlayer(Player p) {
m_gameBar.removePlayer(p);
m_waveBar.removePlayer(p);
m_bombBar.removePlayer(p);
}
public void update() {
switch(m_runner.getStage()) {
case Idle:
m_gameBar.setProgress(1.0);
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("Wave starting!");
m_waveBar.setProgress(m_countdownProgress);
m_bombBar.setVisible(false);
break;
case Playing:
m_gameBar.setProgress(m_waves.progress());
m_gameBar.setTitle("Wave " + m_waves.currentWaveNum() + " / " + m_game.getWaveCount());
m_gameBar.setColor(BarColor.PURPLE);
if (m_mobs.createdMobs() > 0) {
m_waveBar.setVisible(true);
m_waveBar.setColor(BarColor.GREEN);
m_waveBar.setTitle("Mobs remaining: " + m_mobs.remainingMobs());
m_waveBar.setProgress(m_mobs.progress());
} else {
m_waveBar.setVisible(false);
}
if (m_fuse.isLit()) {
m_bombBar.setVisible(true);
m_bombBar.setProgress(1.0 - m_fuse.getProgress());
} else {
m_bombBar.setVisible(false);
}
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;
}
}
}

View File

@ -0,0 +1,15 @@
package gg.malloc.defense.engine;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.Material;
public class Items {
static ItemStack makeBombHelmet() {
ItemStack bombItem = new ItemStack(Material.CARVED_PUMPKIN);
ItemMeta meta = bombItem.getItemMeta();
meta.setCustomModelData(0);
bombItem.setItemMeta(meta);
return bombItem;
}
}