added basic towerdefense logic

This commit is contained in:
Lars Neuhaus 2025-04-12 19:22:37 +02:00
parent 36c6c93edb
commit 38c944e6c1
6 changed files with 299 additions and 27 deletions

View File

@ -0,0 +1,18 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.attribute.Attribute;
record EnemyFactory(EntityType entityType, float health, double speed) {
public EnemyFactory(EntityType entityType) {
this(entityType, 20, 0.3);
}
public EntityCreature buildEntity() {
EntityCreature entity = new EntityCreature(this.entityType);
entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(this.speed);
entity.setHealth(this.health);
return entity;
}
}

View File

@ -0,0 +1,15 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
import net.minestom.server.MinecraftServer;
import net.minestom.server.timer.TaskSchedule;
record GroupFactory(EnemyFactory enemyFactory, int count, long delay) {
public void summonGroup(TowerdefenseRoom instance) {
for (int i = 0; i < this.count; i++) {
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
instance.addEnemy(this.enemyFactory.buildEntity());
return TaskSchedule.stop();
}, TaskSchedule.millis(this.delay*i));
}
}
}

View File

@ -18,23 +18,26 @@ public class Towerdefense extends StatelessGame {
private final Random random = new Random(); private final Random random = new Random();
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 List<TowerdefenseRoom> instances = new ArrayList<>(); private final List<TowerdefenseRoom> instances = new ArrayList<>();
private static final int pathLength = 10;
public Towerdefense() { public Towerdefense() {
super(Dimension.NETHER.key, "Towerdefense", new LastWinsScore()); super(Dimension.NETHER.key, "Towerdefense", new LastWinsScore());
setGenerator(new MazeGenerator()); this.setGenerator(new MazeGenerator());
this.generateMaze(); this.generateMaze();
} }
private void generateMaze() { private void generateMaze() {
Pos position = new Pos(0, 0, 0); Pos position = new Pos(0, 0, 0);
this.addMazePosition(position, Block.GREEN_WOOL); this.addMazePosition(position, Block.GREEN_WOOL);
position = position.add(0,0,2);
List<Integer> previousDirections = new ArrayList<>(); List<Integer> previousDirections = new ArrayList<>();
int direction = 1; // 0 -> right; 1 -> straight; 2 -> left int direction = 1; // 0 -> right; 1 -> straight; 2 -> left
for (int i = 0; i < 9; i++) { for (int i = 0; i < pathLength; i++) {
for (int j = 0; j < 3; j++) { for (int j = 0; j < 9; j++) {
position = position.add(direction-1,0,direction%2); position = position.add(direction-1,0,direction%2);
this.addMazePosition(position, Block.WHITE_WOOL); this.addMazePosition(position, Block.WHITE_WOOL);
} }
@ -44,16 +47,20 @@ public class Towerdefense extends StatelessGame {
long rightLeftDifference = previousDirections.stream().filter(integer -> integer == 0).count() - previousDirections.stream().filter(integer -> integer == 2).count(); long rightLeftDifference = previousDirections.stream().filter(integer -> integer == 0).count() - previousDirections.stream().filter(integer -> integer == 2).count();
if(rightLeftDifference >= 2 || direction == 2) origin = 1; if(rightLeftDifference >= 2 || direction == 2) origin = 1;
if(rightLeftDifference <= -2 || direction == 0) bound = 2; if(rightLeftDifference <= -2 || direction == 0) bound = 2;
direction = random.nextInt(origin, bound); direction = this.random.nextInt(origin, bound);
previousDirections.add(direction); previousDirections.add(direction);
} }
this.addMazePosition(position, Block.WHITE_WOOL); this.addMazePosition(position, Block.WHITE_WOOL);
this.addMazePosition(position.add(0,0,1), Block.WHITE_WOOL); this.addMazePosition(position.add(0,0,3), Block.WHITE_WOOL);
this.addMazePosition(position.add(0,0,2), Block.RED_WOOL); this.addMazePosition(position.add(0,0,6), Block.RED_WOOL);
} }
private void addMazePosition(Pos position, Block pathBlock) { private void addMazePosition(Pos position, Block pathBlock) {
this.mazeBatch.setBlock(position, pathBlock); for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
this.mazeBatch.setBlock(position.add(i-1,0,j-1), pathBlock);
}
}
this.mazePath.add(position.add(0.5,1,0.5)); this.mazePath.add(position.add(0.5,1,0.5));
} }
@ -70,7 +77,10 @@ public class Towerdefense extends StatelessGame {
TowerdefenseRoom newRoom = new TowerdefenseRoom(p, this); TowerdefenseRoom newRoom = new TowerdefenseRoom(p, this);
this.instances.add(newRoom); this.instances.add(newRoom);
p.setInstance(newRoom); p.setInstance(newRoom);
newRoom.startWave(List.of(EntityType.ENDERMAN, EntityType.BLAZE, EntityType.PLAYER, EntityType.HORSE, EntityType.ARMOR_STAND, EntityType.SKELETON)); newRoom.startWave(List.of(
new GroupFactory(new EnemyFactory(EntityType.VILLAGER, 20, 0.1), 5, 800),
new GroupFactory(new EnemyFactory(EntityType.BEE, 10, 1), 3, 400)
));
return false; return false;
} }
} }

View File

@ -2,21 +2,35 @@ 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.util.BatchUtil; import eu.mhsl.minenet.minigames.util.BatchUtil;
import eu.mhsl.minenet.minigames.util.CommonEventHandles;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.*; 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.event.entity.EntityDeathEvent;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.player.PlayerTickEvent;
import net.minestom.server.event.player.PlayerUseItemEvent;
import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material; import net.minestom.server.item.Material;
import net.minestom.server.timer.TaskSchedule;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
public class TowerdefenseRoom extends InstanceContainer { public class TowerdefenseRoom extends InstanceContainer {
private final Player player; private final Player player;
private final Towerdefense game; private final Towerdefense game;
private final List<EntityCreature> enemies = new ArrayList<>();
private final List<Tower> towers = new ArrayList<>();
private final Entity cursor;
public TowerdefenseRoom(Player player, Towerdefense game) { public TowerdefenseRoom(Player player, Towerdefense game) {
super(UUID.randomUUID(), Dimension.OVERWORLD.key); super(UUID.randomUUID(), Dimension.OVERWORLD.key);
@ -25,34 +39,75 @@ public class TowerdefenseRoom extends InstanceContainer {
this.game = game; this.game = game;
this.player.setGameMode(GameMode.ADVENTURE); this.player.setGameMode(GameMode.ADVENTURE);
this.player.setAllowFlying(true); this.player.setAllowFlying(true);
this.player.getInventory().setItemStack(0, ItemStack.of(Material.ARMOR_STAND)); this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(200);
this.player.getInventory().setItemStack(0, ItemStack.of(Material.SKELETON_SPAWN_EGG));
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.setInstance(this);
this.eventNode()
.addListener(EntityDeathEvent.class, event -> {
if(!(event.getEntity() instanceof EntityCreature enemy)) return;
this.enemies.remove(enemy);
})
.addListener(PlayerTickEvent.class, this::setCursorPosition)
.addListener(PlayerUseItemEvent.class, this::setTower)
.addListener(ItemDropEvent.class, CommonEventHandles::cancel);
} }
public void startWave(List<EntityType> entities) { private void setTower(PlayerUseItemEvent event) {
int counter = 0; if(event.getItemStack().material().equals(Material.SKELETON_SPAWN_EGG)) {
for(EntityType entityType : entities) { Point newPosition = this.player.getTargetBlockPosition(20);
MinecraftServer.getSchedulerManager().scheduleTask(() -> { if(newPosition == null) return;
this.addEntity(new EntityCreature(entityType)); if(!this.getBlock(newPosition).equals(Block.BLACK_WOOL)) return;
return TaskSchedule.stop(); this.setBlock(newPosition, Block.BLUE_WOOL);
}, TaskSchedule.millis(800L*counter)); newPosition = newPosition.add(0.5,1,0.5);
counter++; SkeletonTower tower = new SkeletonTower();
tower.setInstance(this, newPosition);
} }
} }
private void addEntity(EntityCreature entity) { public List<EntityCreature> getEnemies() {
entity.setInstance(this, this.game.getMazePath().getFirst()); return this.enemies;
entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.15);
entity.getNavigator().setPathTo(this.game.getMazePath().get(1), 0.7, () -> changeEntityGoal(entity, 1));
} }
private void changeEntityGoal(EntityCreature entity, int positionIndex) { private void setCursorPosition(PlayerTickEvent event) {
if(positionIndex == this.game.getMazePath().size()-1) { Point newPosition = this.player.getTargetBlockPosition(20);
if(newPosition == null) return;
if(this.getBlock(newPosition).equals(Block.BLACK_WOOL)) {
this.cursor.setInvisible(false);
} else {
this.cursor.setInvisible(true);
return; return;
} }
entity.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.7, () -> changeEntityGoal(entity, positionIndex+1)); newPosition = newPosition.add(0.5,1,0.5);
this.cursor.teleport(new Pos(newPosition));
} }
void startWave(List<GroupFactory> enemyGroups) {
enemyGroups.forEach(groupFactory -> groupFactory.summonGroup(this));
}
public void addEnemy(EntityCreature enemy) {
enemy.setInstance(this, this.game.getMazePath().getFirst());
this.enemies.add(enemy);
this.changeEnemyGoal(enemy, 0);
}
public Towerdefense getGame() {
return this.game;
}
private void changeEnemyGoal(EntityCreature enemy, int positionIndex) {
if(positionIndex == this.game.getMazePath().size()-1) {
this.enemies.remove(enemy);
enemy.remove();
this.player.damage(DamageType.PLAYER_ATTACK, enemy.getHealth()/10);
return;
}
enemy.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.6, () -> this.changeEnemyGoal(enemy, positionIndex+1));
}
} }

View File

@ -0,0 +1,24 @@
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.attribute.Attribute;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public class SkeletonTower extends Tower {
public SkeletonTower() {
super(EntityType.SKELETON, 1, 300, 20);
this.setItemInMainHand(ItemStack.of(Material.BOW));
}
@Override
protected void shoot(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
}
@Override
protected void onEnemyHit(EntityCreature enemy) {
enemy.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue()*0.5);
}
}

View File

@ -0,0 +1,150 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseRoom;
import net.minestom.server.MinecraftServer;
import net.minestom.server.collision.Aerodynamics;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.*;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.List;
import java.util.stream.Stream;
public abstract class Tower extends EntityCreature {
public enum Priority {
FIRST(Type.PATH_DISTANCE),
LAST(Type.PATH_DISTANCE),
STRONG(Type.HEALTH),
WEAK(Type.HEALTH),
CLOSE(Type.TOWER_DISTANCE),
FAR(Type.TOWER_DISTANCE);
final Type type;
Priority(Type type) {
this.type = type;
}
public Type getType() {
return this.type;
}
public enum Type {
PATH_DISTANCE,
HEALTH,
TOWER_DISTANCE
}
}
private Priority priority = Priority.FIRST;
protected float damage;
protected int range;
protected TaskSchedule attackDelay;
private final Task shootTask;
public Tower(@NotNull EntityType entityType, float damage, int attackDelay, int range) {
super(entityType);
this.damage = damage;
this.range = range;
this.attackDelay = TaskSchedule.millis(attackDelay);
this.shootTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> {
EntityCreature nextEnemy = this.getNextEnemy();
if(nextEnemy == null) return this.attackDelay;
this.lookAt(nextEnemy);
this.shoot(nextEnemy);
return this.attackDelay;
}, TaskSchedule.immediate());
}
protected void shootProjectile(EntityCreature enemy, EntityType projectileType, double power, double spread) {
EntityProjectile projectile = new EntityProjectile(this, projectileType);
projectile.setView(this.getPosition().yaw(), this.getPosition().pitch());
projectile.setNoGravity(true);
projectile.setAerodynamics(new Aerodynamics(0, 1, 0));
Pos startingPoint = this.getPosition().add(0, this.getEyeHeight(), 0);
double enemySpeed = enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue() / 0.05;
Point enemyGoal = enemy.getNavigator().getGoalPosition();
if(enemyGoal == null) enemyGoal = enemy.getPosition();
Pos enemyPosition = enemy.getPosition();
Vec enemyMovement = Vec.fromPoint(enemyGoal.sub(enemyPosition)).normalize().mul(enemySpeed);
// just an approximation, not calculated correctly:
double projectileSpeed = 20 * power;
double enemyTowerDistance = startingPoint.distance(enemyPosition);
double estimatedFlightTime = (enemyTowerDistance / projectileSpeed);
Pos targetPosition = enemyPosition.add(enemyMovement.mul(estimatedFlightTime)).withY(enemyPosition.y()+enemy.getEyeHeight());
projectile.shoot(targetPosition, power, spread);
projectile.scheduleRemove(Duration.ofSeconds(5));
projectile.setInstance(this.getInstance(), startingPoint);
projectile.eventNode()
.addListener(ProjectileCollideWithEntityEvent.class, event -> {
if(!(event.getTarget() instanceof EntityCreature target)) return;
if(!this.getRoomInstance().getEnemies().contains(target)) return;
target.damage(DamageType.PLAYER_ATTACK, this.damage);
this.onEnemyHit(target);
projectile.remove();
})
.addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove());
}
protected void onEnemyHit(EntityCreature enemy) {
}
protected TowerdefenseRoom getRoomInstance() {
return (TowerdefenseRoom) this.getInstance();
}
private @Nullable EntityCreature getNextEnemy() {
List<EntityCreature> enemies = this.getSortedEnemies();
if(enemies.isEmpty()) return null;
return switch (this.priority) {
case LAST, STRONG, FAR -> enemies.getLast();
case FIRST, WEAK, CLOSE -> enemies.getFirst();
};
}
private List<EntityCreature> getSortedEnemies() {
Stream<EntityCreature> enemyStream = this.getRoomInstance().getEnemies().stream().parallel()
.filter(enemy -> !enemy.isDead())
.filter(enemy -> enemy.getPosition().distance(this.getPosition()) <= this.range);
return switch (this.priority.getType()) {
case PATH_DISTANCE -> enemyStream
.sorted((o1, o2) -> {
Pos endPoint = this.getRoomInstance().getGame().getMazePath().getLast();
return Double.compare(
o1.getPosition().distance(endPoint),
o2.getPosition().distance(endPoint)
);
}).toList();
case TOWER_DISTANCE -> enemyStream
.sorted((o1, o2) -> Double.compare(
o1.getPosition().distance(this.getPosition()),
o2.getPosition().distance(this.getPosition())
)).toList();
case HEALTH -> enemyStream
.sorted((o1, o2) -> Float.compare(o1.getHealth(), o2.getHealth()))
.toList();
};
}
@Override
protected void remove(boolean permanent) {
this.shootTask.cancel();
super.remove(permanent);
}
protected abstract void shoot(EntityCreature enemy);
}