added towers and added tower spawn mechanics

This commit is contained in:
Lars Neuhaus 2025-04-12 23:48:06 +02:00
parent 38c944e6c1
commit 893923cc56
7 changed files with 106 additions and 33 deletions

View File

@ -5,6 +5,16 @@ import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.attribute.Attribute; import net.minestom.server.entity.attribute.Attribute;
record EnemyFactory(EntityType entityType, float health, double speed) { record EnemyFactory(EntityType entityType, float health, double speed) {
/**
* Factory for a tower defense enemy.
* @param entityType type of enemy
* @param health base health, between 0 and 1024
* @param speed walk speed of enemy
*/
public EnemyFactory {
if(health > 1024 || health <= 0) throw new IllegalArgumentException("Enemy health has to be between 0 and 1024");
}
public EnemyFactory(EntityType entityType) { public EnemyFactory(EntityType entityType) {
this(entityType, 20, 0.3); this(entityType, 20, 0.3);
} }
@ -12,6 +22,7 @@ record EnemyFactory(EntityType entityType, float health, double speed) {
public EntityCreature buildEntity() { public EntityCreature buildEntity() {
EntityCreature entity = new EntityCreature(this.entityType); EntityCreature entity = new EntityCreature(this.entityType);
entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(this.speed); entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(this.speed);
entity.getAttribute(Attribute.MAX_HEALTH).setBaseValue(this.health);
entity.setHealth(this.health); entity.setHealth(this.health);
return entity; return entity;
} }

View File

@ -3,15 +3,21 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.BlazeTower;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.SkeletonTower;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.Tower;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.ZombieTower;
import eu.mhsl.minenet.minigames.score.LastWinsScore; import eu.mhsl.minenet.minigames.score.LastWinsScore;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.EntityType; import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.instance.batch.AbsoluteBlockBatch; import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Material;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
public class Towerdefense extends StatelessGame { public class Towerdefense extends StatelessGame {
@ -19,6 +25,11 @@ public class Towerdefense extends StatelessGame {
private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch(); private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch();
private final List<Pos> mazePath = new ArrayList<>(); private final List<Pos> mazePath = new ArrayList<>();
private final List<TowerdefenseRoom> instances = new ArrayList<>(); private final List<TowerdefenseRoom> instances = new ArrayList<>();
private final Map<Material, Class<? extends Tower>> availableTowers = Map.of(
Material.SKELETON_SPAWN_EGG, SkeletonTower.class,
Material.ZOMBIE_SPAWN_EGG, ZombieTower.class,
Material.BLAZE_SPAWN_EGG, BlazeTower.class
);
private static final int pathLength = 10; private static final int pathLength = 10;
@ -79,8 +90,13 @@ public class Towerdefense extends StatelessGame {
p.setInstance(newRoom); p.setInstance(newRoom);
newRoom.startWave(List.of( newRoom.startWave(List.of(
new GroupFactory(new EnemyFactory(EntityType.VILLAGER, 20, 0.1), 5, 800), new GroupFactory(new EnemyFactory(EntityType.VILLAGER, 20, 0.1), 5, 800),
new GroupFactory(new EnemyFactory(EntityType.BEE, 10, 1), 3, 400) new GroupFactory(new EnemyFactory(EntityType.BEE, 10, 1), 3, 400),
new GroupFactory(new EnemyFactory(EntityType.SNIFFER, 1000, 0.02), 1, 0)
)); ));
return false; return false;
} }
public Map<Material, Class<? extends Tower>> getAvailableTowers() {
return this.availableTowers;
}
} }

View File

@ -2,7 +2,6 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.SkeletonTower;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.Tower; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.Tower;
import eu.mhsl.minenet.minigames.util.BatchUtil; import eu.mhsl.minenet.minigames.util.BatchUtil;
import eu.mhsl.minenet.minigames.util.CommonEventHandles; import eu.mhsl.minenet.minigames.util.CommonEventHandles;
@ -13,7 +12,10 @@ import net.minestom.server.entity.*;
import net.minestom.server.entity.attribute.Attribute; import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.damage.DamageType; import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.event.entity.EntityDeathEvent; import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.inventory.InventoryPreClickEvent;
import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.player.PlayerEntityInteractEvent;
import net.minestom.server.event.player.PlayerSwapItemEvent;
import net.minestom.server.event.player.PlayerTickEvent; import net.minestom.server.event.player.PlayerTickEvent;
import net.minestom.server.event.player.PlayerUseItemEvent; import net.minestom.server.event.player.PlayerUseItemEvent;
import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceContainer;
@ -40,13 +42,15 @@ public class TowerdefenseRoom extends InstanceContainer {
this.player.setGameMode(GameMode.ADVENTURE); this.player.setGameMode(GameMode.ADVENTURE);
this.player.setAllowFlying(true); this.player.setAllowFlying(true);
this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(200); this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(200);
this.player.getInventory().setItemStack(0, ItemStack.of(Material.SKELETON_SPAWN_EGG));
this.game.getAvailableTowers().keySet().forEach(material -> this.player.getInventory().addItemStack(ItemStack.of(material)));
this.setGenerator(new MazeGenerator()); this.setGenerator(new MazeGenerator());
BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {}); BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {});
this.cursor = new Entity(EntityType.ARMOR_STAND); this.cursor = new Entity(EntityType.ARMOR_STAND);
this.cursor.setInstance(this); this.cursor.setInstance(this);
this.cursor.setBoundingBox(0,0,0);
this.eventNode() this.eventNode()
.addListener(EntityDeathEvent.class, event -> { .addListener(EntityDeathEvent.class, event -> {
@ -54,19 +58,27 @@ public class TowerdefenseRoom extends InstanceContainer {
this.enemies.remove(enemy); this.enemies.remove(enemy);
}) })
.addListener(PlayerTickEvent.class, this::setCursorPosition) .addListener(PlayerTickEvent.class, this::setCursorPosition)
.addListener(PlayerUseItemEvent.class, this::setTower) .addListener(PlayerUseItemEvent.class, event -> this.setTower(event.getItemStack().material()))
.addListener(ItemDropEvent.class, CommonEventHandles::cancel); .addListener(PlayerEntityInteractEvent.class, event -> this.setTower(event.getPlayer().getItemInMainHand().material()))
.addListener(ItemDropEvent.class, CommonEventHandles::cancel)
.addListener(InventoryPreClickEvent.class, CommonEventHandles::cancel)
.addListener(PlayerSwapItemEvent.class, CommonEventHandles::cancel);
} }
private void setTower(PlayerUseItemEvent event) { private void setTower(Material usedMaterial) {
if(event.getItemStack().material().equals(Material.SKELETON_SPAWN_EGG)) { if(!this.canPlaceTower()) return;
Point newPosition = this.player.getTargetBlockPosition(20); try {
if(newPosition == null) return; Tower tower = this.game.getAvailableTowers().entrySet().stream()
if(!this.getBlock(newPosition).equals(Block.BLACK_WOOL)) return; .filter(materialClassEntry -> materialClassEntry.getKey().equals(usedMaterial))
this.setBlock(newPosition, Block.BLUE_WOOL); .findFirst()
newPosition = newPosition.add(0.5,1,0.5); .orElseThrow()
SkeletonTower tower = new SkeletonTower(); .getValue()
tower.setInstance(this, newPosition); .getConstructor()
.newInstance();
this.setBlock(this.cursor.getPosition().sub(0, 0.5, 0), Block.BLUE_WOOL);
tower.setInstance(this, this.cursor.getPosition());
this.towers.add(tower);
} catch (Exception ignored) {
} }
} }
@ -77,14 +89,13 @@ public class TowerdefenseRoom extends InstanceContainer {
private void setCursorPosition(PlayerTickEvent event) { private void setCursorPosition(PlayerTickEvent event) {
Point newPosition = this.player.getTargetBlockPosition(20); Point newPosition = this.player.getTargetBlockPosition(20);
if(newPosition == null) return; if(newPosition == null) return;
if(this.getBlock(newPosition).equals(Block.BLACK_WOOL)) {
this.cursor.setInvisible(false);
} else {
this.cursor.setInvisible(true);
return;
}
newPosition = newPosition.add(0.5,1,0.5); newPosition = newPosition.add(0.5,1,0.5);
this.cursor.teleport(new Pos(newPosition)); this.cursor.teleport(new Pos(newPosition));
this.cursor.setInvisible(!this.canPlaceTower());
}
private boolean canPlaceTower() {
return this.getBlock(this.cursor.getPosition().sub(0, 1, 0)).equals(Block.BLACK_WOOL);
} }
void startWave(List<GroupFactory> enemyGroups) { void startWave(List<GroupFactory> enemyGroups) {

View File

@ -0,0 +1,15 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
public class BlazeTower extends Tower {
public BlazeTower() {
super(EntityType.BLAZE, 2, 1, 20);
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.FIREBALL, 2, 0);
}
}

View File

@ -2,23 +2,23 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.tow
import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType; import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material; import net.minestom.server.item.Material;
public class SkeletonTower extends Tower { public class SkeletonTower extends Tower {
public SkeletonTower() { public SkeletonTower() {
super(EntityType.SKELETON, 1, 300, 20); super(EntityType.SKELETON, 1, 3, 15);
this.setItemInMainHand(ItemStack.of(Material.BOW)); this.setItemInMainHand(ItemStack.of(Material.BOW));
} }
@Override @Override
protected void shoot(EntityCreature enemy) { protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.ARROW, 2, 0); this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
} }
@Override @Override
protected void onEnemyHit(EntityCreature enemy) { protected void onProjectileHit(EntityCreature enemy) {
enemy.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue()*0.5); super.onProjectileHit(enemy);
// enemy.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue()*0.5);
} }
} }

View File

@ -45,22 +45,23 @@ public abstract class Tower extends EntityCreature {
} }
} }
private final static int damageDivider = 10;
private Priority priority = Priority.FIRST; private Priority priority = Priority.FIRST;
protected float damage; protected float damage;
protected int range; protected int range;
protected TaskSchedule attackDelay; protected TaskSchedule attackDelay;
private final Task shootTask; private final Task shootTask;
public Tower(@NotNull EntityType entityType, float damage, int attackDelay, int range) { public Tower(@NotNull EntityType entityType, int damage, double attacksPerSecond, int range) {
super(entityType); super(entityType);
this.damage = damage; this.damage = (float) damage / damageDivider;
this.range = range; this.range = range;
this.attackDelay = TaskSchedule.millis(attackDelay); this.attackDelay = TaskSchedule.millis((long) (1000/attacksPerSecond));
this.shootTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> { this.shootTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> {
EntityCreature nextEnemy = this.getNextEnemy(); EntityCreature nextEnemy = this.getNextEnemy();
if(nextEnemy == null) return this.attackDelay; if(nextEnemy == null) return this.attackDelay;
this.lookAt(nextEnemy); this.lookAt(nextEnemy);
this.shoot(nextEnemy); this.attack(nextEnemy);
return this.attackDelay; return this.attackDelay;
}, TaskSchedule.immediate()); }, TaskSchedule.immediate());
} }
@ -92,14 +93,13 @@ public abstract class Tower extends EntityCreature {
if(!(event.getTarget() instanceof EntityCreature target)) return; if(!(event.getTarget() instanceof EntityCreature target)) return;
if(!this.getRoomInstance().getEnemies().contains(target)) return; if(!this.getRoomInstance().getEnemies().contains(target)) return;
target.damage(DamageType.PLAYER_ATTACK, this.damage); target.damage(DamageType.PLAYER_ATTACK, this.damage);
this.onEnemyHit(target); this.onProjectileHit(target);
projectile.remove(); projectile.remove();
}) })
.addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove()); .addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove());
} }
protected void onEnemyHit(EntityCreature enemy) { protected void onProjectileHit(EntityCreature enemy) {
} }
protected TowerdefenseRoom getRoomInstance() { protected TowerdefenseRoom getRoomInstance() {
@ -146,5 +146,5 @@ public abstract class Tower extends EntityCreature {
super.remove(permanent); super.remove(permanent);
} }
protected abstract void shoot(EntityCreature enemy); protected abstract void attack(EntityCreature enemy);
} }

View File

@ -0,0 +1,20 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public class ZombieTower extends Tower {
public ZombieTower() {
super(EntityType.ZOMBIE, 3, 0.7, 4);
this.setItemInMainHand(ItemStack.of(Material.IRON_SWORD));
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
enemy.damage(DamageType.PLAYER_ATTACK, this.damage);
}
}