diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/EnemyFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/EnemyFactory.java new file mode 100644 index 0000000..a1a6d49 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/EnemyFactory.java @@ -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; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/GroupFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/GroupFactory.java new file mode 100644 index 0000000..94decd9 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/GroupFactory.java @@ -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)); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/Towerdefense.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/Towerdefense.java index 6fec923..f51b4a5 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/Towerdefense.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/Towerdefense.java @@ -18,23 +18,26 @@ public class Towerdefense extends StatelessGame { private final Random random = new Random(); private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch(); private final List mazePath = new ArrayList<>(); - private List instances = new ArrayList<>(); + private final List instances = new ArrayList<>(); + + private static final int pathLength = 10; public Towerdefense() { super(Dimension.NETHER.key, "Towerdefense", new LastWinsScore()); - setGenerator(new MazeGenerator()); + this.setGenerator(new MazeGenerator()); this.generateMaze(); } private void generateMaze() { Pos position = new Pos(0, 0, 0); this.addMazePosition(position, Block.GREEN_WOOL); + position = position.add(0,0,2); List previousDirections = new ArrayList<>(); int direction = 1; // 0 -> right; 1 -> straight; 2 -> left - for (int i = 0; i < 9; i++) { - for (int j = 0; j < 3; j++) { + for (int i = 0; i < pathLength; i++) { + for (int j = 0; j < 9; j++) { position = position.add(direction-1,0,direction%2); 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(); if(rightLeftDifference >= 2 || direction == 2) origin = 1; if(rightLeftDifference <= -2 || direction == 0) bound = 2; - direction = random.nextInt(origin, bound); + direction = this.random.nextInt(origin, bound); previousDirections.add(direction); } this.addMazePosition(position, Block.WHITE_WOOL); - this.addMazePosition(position.add(0,0,1), Block.WHITE_WOOL); - this.addMazePosition(position.add(0,0,2), Block.RED_WOOL); + this.addMazePosition(position.add(0,0,3), Block.WHITE_WOOL); + this.addMazePosition(position.add(0,0,6), Block.RED_WOOL); } 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)); } @@ -70,7 +77,10 @@ public class Towerdefense extends StatelessGame { TowerdefenseRoom newRoom = new TowerdefenseRoom(p, this); this.instances.add(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; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseRoom.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseRoom.java index 9b02e0f..8dca24d 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseRoom.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseRoom.java @@ -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.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.CommonEventHandles; 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.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.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; -import net.minestom.server.timer.TaskSchedule; +import java.util.ArrayList; import java.util.List; import java.util.UUID; public class TowerdefenseRoom extends InstanceContainer { private final Player player; private final Towerdefense game; + private final List enemies = new ArrayList<>(); + private final List towers = new ArrayList<>(); + private final Entity cursor; public TowerdefenseRoom(Player player, Towerdefense game) { super(UUID.randomUUID(), Dimension.OVERWORLD.key); @@ -25,34 +39,75 @@ public class TowerdefenseRoom extends InstanceContainer { this.game = game; this.player.setGameMode(GameMode.ADVENTURE); 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, () -> {}); + + 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 entities) { - int counter = 0; - for(EntityType entityType : entities) { - MinecraftServer.getSchedulerManager().scheduleTask(() -> { - this.addEntity(new EntityCreature(entityType)); - return TaskSchedule.stop(); - }, TaskSchedule.millis(800L*counter)); - counter++; + private void setTower(PlayerUseItemEvent event) { + if(event.getItemStack().material().equals(Material.SKELETON_SPAWN_EGG)) { + Point newPosition = this.player.getTargetBlockPosition(20); + if(newPosition == null) return; + if(!this.getBlock(newPosition).equals(Block.BLACK_WOOL)) return; + this.setBlock(newPosition, Block.BLUE_WOOL); + newPosition = newPosition.add(0.5,1,0.5); + SkeletonTower tower = new SkeletonTower(); + tower.setInstance(this, newPosition); } } - private void addEntity(EntityCreature entity) { - entity.setInstance(this, this.game.getMazePath().getFirst()); - entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.15); - entity.getNavigator().setPathTo(this.game.getMazePath().get(1), 0.7, () -> changeEntityGoal(entity, 1)); + public List getEnemies() { + return this.enemies; } - private void changeEntityGoal(EntityCreature entity, int positionIndex) { - if(positionIndex == this.game.getMazePath().size()-1) { + private void setCursorPosition(PlayerTickEvent event) { + 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; } - 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 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)); + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/towers/SkeletonTower.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/towers/SkeletonTower.java new file mode 100644 index 0000000..30c012c --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/towers/SkeletonTower.java @@ -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); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/towers/Tower.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/towers/Tower.java new file mode 100644 index 0000000..c26e09c --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/towers/Tower.java @@ -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 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 getSortedEnemies() { + Stream 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); +}