14 Commits

Author SHA1 Message Date
460d737d3d reset player level 2025-07-19 02:21:23 +02:00
7591ce29ff added strong towers 2025-07-19 02:09:55 +02:00
39bbf19d9d reset more player values in Room 2025-07-19 01:48:35 +02:00
df239d8166 made armadillo faster and chicken slower 2025-07-19 01:27:35 +02:00
bf9c16ccc8 removed log 2025-07-19 01:24:40 +02:00
b5d6d7bac4 added wave mechanic 2025-07-19 01:10:57 +02:00
3b55f16b24 added range to cursor 2025-04-16 23:43:46 +02:00
5ef254b75f fixed tower spawning issues 2025-04-14 17:26:59 +02:00
5e5f36153e added own cursor class 2025-04-13 23:28:46 +02:00
e77879c657 added tower value calculation and delete tower mechanic 2025-04-13 18:35:50 +02:00
e3068be160 moved shooting mechanic to ShootingTower, added money 2025-04-13 16:12:08 +02:00
867bee1c5a corrected PlayerLoginHandler 2025-04-13 13:07:28 +02:00
893923cc56 added towers and added tower spawn mechanics 2025-04-12 23:48:06 +02:00
38c944e6c1 added basic towerdefense logic 2025-04-12 19:22:37 +02:00
17 changed files with 839 additions and 47 deletions

View File

@ -2,7 +2,6 @@ package eu.mhsl.minenet.minigames.handler.global;
import eu.mhsl.minenet.minigames.Main;
import eu.mhsl.minenet.minigames.api.QueuedPlayerRooms;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.instance.transfer.Transfer;
import eu.mhsl.minenet.minigames.skin.SkinCache;
@ -49,15 +48,7 @@ public class PlayerLoginHandler implements EventListener<AsyncPlayerConfiguratio
if(pushQueue != null) {
Room.setRoom(p, Room.getRoom(pushQueue).orElseThrow());
} else {
if(p.getUsername().equals("28Pupsi28")) {
try {
MoveInstance.move(p, new TowerdefenseFactory().manufacture(Room.createRoom(p)));
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
MoveInstance.move(p, Hub.INSTANCE);
}
MoveInstance.move(p, Hub.INSTANCE);
}
},
TaskSchedule.seconds(1),

View File

@ -0,0 +1,89 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.Tower;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Material;
import net.minestom.server.network.packet.server.SendablePacket;
import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
public class Cursor extends Entity {
private boolean cursorActive = false;
private final int reach;
public Cursor(int reach) {
super(EntityType.ARMOR_STAND);
this.setBoundingBox(0,0,0);
this.reach = reach;
}
public void updateCursorPosition(Player player) {
Point targetPosition = player.getTargetBlockPosition(this.reach);
this.moveCursorTo(targetPosition);
}
private void moveCursorTo(@Nullable Point newTargetBlockPosition) {
this.setCursorEnabled(true);
if(newTargetBlockPosition == null) {
this.setCursorEnabled(false);
return;
}
if(!this.getInstance().getBlock(newTargetBlockPosition).equals(Block.BLACK_WOOL)) this.setCursorEnabled(false);
Material holdingMaterial = this.getInstance().getPlayerHandMaterial();
if(!this.getInstance().getGame().getAvailableTowers().containsKey(holdingMaterial)) this.setCursorEnabled(false);
this.teleport(new Pos(newTargetBlockPosition.add(0.5,1,0.5)));
this.showRange(this.getInstance().getGame().getAvailableTowers().get(holdingMaterial));
}
private void setCursorEnabled(boolean enabled) {
this.cursorActive = enabled;
this.setInvisible(!enabled);
}
private void showRange(@Nullable Class<? extends Tower> towerClass) {
if(towerClass == null) return;
if(!this.isCursorActive()) return;
try {
int range = towerClass.getConstructor().newInstance().getRange();
Collection<SendablePacket> particles = new ArrayList<>();
double circumference = 2 * Math.PI * range;
int count = (int) (circumference * 1.5);
for (int i = 0; i < count; i++) {
double radians = ((2 * Math.PI) / count) * i;
Vec relativePosition = new Vec(Math.sin(radians)*range, 0, Math.cos(radians)*range);
ParticlePacket particle = new ParticlePacket(Particle.COMPOSTER, this.position.add(relativePosition), Pos.ZERO, 0, 1);
particles.add(particle);
}
this.getInstance().getPlayer().sendPackets(particles);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public TowerdefenseRoom getInstance() {
return (TowerdefenseRoom) super.getInstance();
}
public boolean isCursorActive() {
return this.cursorActive;
}
public Block getTargetBlock() {
return this.getInstance().getBlock(this.getTargetBlockPosition());
}
public Pos getTargetBlockPosition() {
return this.getPosition().sub(0, 0.5, 0);
}
}

View File

@ -2,39 +2,93 @@ 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.StatelessGame;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies.WaveGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.*;
import eu.mhsl.minenet.minigames.score.LastWinsScore;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Material;
import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class Towerdefense extends StatelessGame {
private final Random random = new Random();
private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch();
private final List<Pos> mazePath = new ArrayList<>();
private List<TowerdefenseRoom> instances = new ArrayList<>();
private final List<TowerdefenseRoom> instances = new ArrayList<>();
private final WaveGenerator waveGenerator;
private Task waveTask;
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,
Material.ENDERMAN_SPAWN_EGG, EndermanTower.class,
Material.PIGLIN_BRUTE_SPAWN_EGG, BruteTower.class,
Material.WITHER_SPAWN_EGG, WitherTower.class,
Material.ENDER_DRAGON_SPAWN_EGG, EnderDragonTower.class
);
private final Map<Class<? extends Tower>, Integer> prices = Map.of(
ZombieTower.class, 4,
SkeletonTower.class, 3,
BlazeTower.class, 8,
EndermanTower.class, 20,
BruteTower.class, 27,
WitherTower.class, 40,
EnderDragonTower.class, 200
);
private static final int pathLength = 10;
public Towerdefense() {
super(Dimension.NETHER.key, "Towerdefense", new LastWinsScore());
setGenerator(new MazeGenerator());
this.setGenerator(new MazeGenerator());
this.generateMaze();
this.waveGenerator = new WaveGenerator(this);
}
@Override
protected void checkAbandoned() {
this.scheduleNextTick((instance) -> {
if(this.instances.stream().allMatch(room -> room.getPlayers().isEmpty()) && this.getPlayers().isEmpty()) {
this.waveTask.cancel();
this.unload();
}
});
}
@Override
protected void onStart() {
this.getPlayers().forEach(player -> {
TowerdefenseRoom newRoom = new TowerdefenseRoom(player, this);
this.instances.add(newRoom);
player.setInstance(newRoom, new Pos(0, 1, 0));
});
this.waveTask = MinecraftServer.getSchedulerManager().scheduleTask(
this.waveGenerator::startNextWave,
TaskSchedule.seconds(3),
TaskSchedule.seconds(25)
);
}
private void generateMaze() {
Pos position = new Pos(0, 0, 0);
this.addMazePosition(position, Block.GREEN_WOOL);
position = position.add(0,0,2);
List<Integer> 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 +98,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));
}
@ -65,12 +123,21 @@ public class Towerdefense extends StatelessGame {
return this.mazePath;
}
public List<TowerdefenseRoom> getInstances() {
return this.instances;
}
@Override
protected boolean onPlayerJoin(Player p) {
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));
return false;
MinecraftServer.getSchedulerManager().scheduleNextTick(() -> p.teleport(new Pos((long) -this.getPlayers().size(), 1, 0)));
return super.onPlayerJoin(p);
}
public Map<Class<? extends Tower>, Integer> getPrices() {
return this.prices;
}
public Map<Material, Class<? extends Tower>> getAvailableTowers() {
return this.availableTowers;
}
}

View File

@ -1,22 +1,47 @@
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.enemies.GroupFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator;
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.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.MinecraftServer;
import net.minestom.server.collision.Aerodynamics;
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.EntityDeathEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.event.inventory.InventoryPreClickEvent;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.item.PlayerBeginItemUseEvent;
import net.minestom.server.event.player.*;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemAnimation;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class TowerdefenseRoom extends InstanceContainer {
private final static int reach = 30;
private long lastPlayerAttack = 0;
private final Player player;
private final Towerdefense game;
private final List<EntityCreature> enemies = new ArrayList<>();
private final List<Tower> towers = new ArrayList<>();
private final Cursor cursor;
private int money = 0;
public TowerdefenseRoom(Player player, Towerdefense game) {
super(UUID.randomUUID(), Dimension.OVERWORLD.key);
@ -25,34 +50,189 @@ 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.addMoney(0);
this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(reach);
this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(reach);
setGenerator(new MazeGenerator());
this.player.getInventory().addItemStack(
ItemStack.of(Material.BOW).withCustomName(Component.text("Schießen", TextColor.color(255, 180, 0)))
);
this.player.getInventory().addItemStack(
ItemStack.of(Material.BARRIER).withCustomName(Component.text("Löschen", TextColor.color(255,0,0)))
);
this.game.getAvailableTowers().forEach((material, tower) -> {
int price = this.game.getPrices().get(tower);
this.player.getInventory().addItemStack(ItemStack.of(material).withMaxStackSize(price).withAmount(price));
});
this.setGenerator(new MazeGenerator());
BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {});
this.cursor = new Cursor(reach);
this.cursor.setInstance(this);
this.cursor.setBoundingBox(0,0,0);
this.eventNode()
.addListener(EntityDeathEvent.class, event -> {
if(!(event.getEntity() instanceof EntityCreature enemy)) return;
this.enemies.remove(enemy);
})
.addListener(PlayerTickEvent.class, event -> this.cursor.updateCursorPosition(this.player))
.addListener(PlayerUseItemEvent.class, event -> this.useItem())
.addListener(PlayerUseItemOnBlockEvent.class, event -> this.useItem())
.addListener(PlayerEntityInteractEvent.class, event -> this.useItem())
.addListener(PlayerHandAnimationEvent.class, event -> this.useItem())
.addListener(ItemDropEvent.class, CommonEventHandles::cancel)
.addListener(InventoryPreClickEvent.class, CommonEventHandles::cancel)
.addListener(PlayerSwapItemEvent.class, CommonEventHandles::cancel)
.addListener(PlayerBeginItemUseEvent.class, event -> {
if(event.getAnimation().equals(ItemAnimation.BOW)) event.setCancelled(true);
});
}
public void startWave(List<EntityType> 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 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));
}
private void changeEntityGoal(EntityCreature entity, int positionIndex) {
if(positionIndex == this.game.getMazePath().size()-1) {
private void useItem() {
Material itemInHand = this.player.getItemInMainHand().material();
if(itemInHand.equals(Material.BOW)) {
this.playerAttack();
return;
}
entity.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.7, () -> changeEntityGoal(entity, positionIndex+1));
if(itemInHand.equals(Material.BARRIER)) {
this.removeTower();
return;
}
this.placeTower();
}
private void playerAttack() {
if(this.lastPlayerAttack > System.currentTimeMillis()-200) return;
Player p = this.player;
this.lastPlayerAttack = System.currentTimeMillis();
EntityProjectile projectile = new EntityProjectile(p, EntityType.SPECTRAL_ARROW);
projectile.setView(p.getPosition().yaw(), p.getPosition().pitch());
projectile.setNoGravity(true);
projectile.setAerodynamics(new Aerodynamics(0, 1, 1));
Vec projectileVelocity = p.getPosition().direction().normalize().mul(20);
projectile.setVelocity(projectileVelocity);
projectile.scheduleRemove(Duration.ofSeconds(5));
projectile.setInstance(this, p.getPosition().add(0, p.getEyeHeight()-0.5, 0));
projectile.eventNode()
.addListener(ProjectileCollideWithEntityEvent.class, hitEvent -> {
if(!(hitEvent.getTarget() instanceof EntityCreature target)) return;
if(!this.getEnemies().stream().filter(entityCreature -> !entityCreature.isDead()).toList().contains(target)) return;
target.damage(DamageType.PLAYER_ATTACK, 0.1f);
if(target.isDead()) this.addMoney((int) target.getAttribute(Attribute.MAX_HEALTH).getBaseValue());
projectile.remove();
})
.addListener(ProjectileCollideWithBlockEvent.class, collisionEvent -> projectile.remove());
}
public void addMoney(int amount) {
this.money += amount;
this.player.setLevel(this.money);
}
public Material getPlayerHandMaterial() {
return this.player.getItemInMainHand().material();
}
private synchronized void placeTower() {
if(!this.canPlaceActiveTower()) {
if(this.cursor.isCursorActive() && !this.enoughMoneyForActiveTower()) {
this.player.sendActionBar(Component.text("Nicht genug Geld!", TextColor.color(255,0,0)));
}
return;
}
try {
Material usedMaterial = this.player.getItemInMainHand().material();
Tower tower = this.game.getAvailableTowers().entrySet().stream()
.filter(materialClassEntry -> materialClassEntry.getKey().equals(usedMaterial))
.findFirst()
.orElseThrow()
.getValue()
.getConstructor()
.newInstance();
this.setBlock(this.cursor.getTargetBlockPosition(), Block.BLUE_WOOL);
tower.setInstance(this, this.cursor.getPosition());
this.towers.add(tower);
tower.startShooting();
this.addMoney(-this.game.getPrices().get(tower.getClass()));
} catch (Exception ignored) {
}
this.cursor.updateCursorPosition(this.player);
}
private void removeTower() {
Entity entity = this.player.getLineOfSightEntity(reach, entity1 -> true);
if(!(entity instanceof Tower tower)) return;
if(!this.towers.contains(tower)) return;
this.setBlock(tower.getPosition().sub(0, 0.5, 0), Block.BLACK_WOOL);
this.addMoney(tower.getSellingPrice(this.game.getPrices().get(tower.getClass())));
this.towers.remove(tower);
tower.remove();
}
private boolean canPlaceActiveTower() {
Material usedMaterial = this.player.getItemInMainHand().material();
return this.canPlaceTower(this.game.getAvailableTowers().get(usedMaterial));
}
private boolean canPlaceTower(@Nullable Class<? extends Tower> towerClass) {
if(towerClass == null) return false;
return this.cursor.isCursorActive() && this.enoughMoney(towerClass);
}
private boolean enoughMoneyForActiveTower() {
Material usedMaterial = this.player.getItemInMainHand().material();
return this.enoughMoney(this.game.getAvailableTowers().get(usedMaterial));
}
private boolean enoughMoney(@Nullable Class<? extends Tower> towerClass) {
if(towerClass == null) return true;
return this.money >= this.game.getPrices().get(towerClass);
}
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);
}
private synchronized void changeEnemyGoal(EntityCreature enemy, int positionIndex) {
if(positionIndex == this.game.getMazePath().size()-1) {
this.enemies.remove(enemy);
enemy.remove();
float damage = (float) Math.ceil(enemy.getHealth()/10);
if(this.player.getHealth() - damage <= 0) {
this.getEnemies().forEach(Entity::remove);
this.towers.forEach(Entity::remove);
this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).attribute().defaultValue());
this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).attribute().defaultValue());
this.player.setInstance(this.game).thenRun(() -> MinecraftServer.getInstanceManager().unregisterInstance(this));
this.player.heal();
this.game.getScore().insertResult(this.player);
return;
}
this.player.damage(DamageType.PLAYER_ATTACK, damage);
return;
}
enemy.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.6, () -> this.changeEnemyGoal(enemy, positionIndex+1));
}
public Towerdefense getGame() {
return this.game;
}
public List<EntityCreature> getEnemies() {
return this.enemies;
}
public Player getPlayer() {
return this.player;
}
}

View File

@ -0,0 +1,33 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.attribute.Attribute;
public 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, default 10)
* @param speed walk speed (default 0.1)
*/
public EnemyFactory {
if(health > 1024 || health <= 0) throw new IllegalArgumentException("Enemy health has to be between 0 and 1024");
}
public EnemyFactory(EntityType entityType) {
this(entityType, 10, 0.1);
}
public EntityCreature buildEntity() {
EntityCreature entity = new EntityCreature(this.entityType);
entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(this.speed);
entity.getAttribute(Attribute.MAX_HEALTH).setBaseValue(this.health);
entity.setHealth(this.health);
return entity;
}
public int getStrength() {
return (int) Math.floor(this.health * this.speed * 5);
}
}

View File

@ -0,0 +1,16 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseRoom;
import net.minestom.server.MinecraftServer;
import net.minestom.server.timer.TaskSchedule;
public 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

@ -0,0 +1,77 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.Towerdefense;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.EntityType;
import net.minestom.server.timer.TaskSchedule;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
public class WaveGenerator {
final Towerdefense game;
int currentLevel;
Random random = new Random();
private final Map<EnemyFactory, Integer> availableEnemies = Map.of(
new EnemyFactory(EntityType.PIG, 2, 0.1), new EnemyFactory(EntityType.PIG, 2, 0.1).getStrength(),
new EnemyFactory(EntityType.VILLAGER, 6, 0.1), new EnemyFactory(EntityType.VILLAGER, 6, 0.1).getStrength(),
new EnemyFactory(EntityType.SHEEP, 4, 0.2), new EnemyFactory(EntityType.SHEEP, 4, 0.2).getStrength(),
new EnemyFactory(EntityType.COW, 10, 0.1), new EnemyFactory(EntityType.COW, 10, 0.1).getStrength(),
new EnemyFactory(EntityType.CAMEL, 10, 0.2), new EnemyFactory(EntityType.CAMEL, 10, 0.2).getStrength(),
new EnemyFactory(EntityType.ARMADILLO, 20, 0.4), new EnemyFactory(EntityType.ARMADILLO, 20, 0.3).getStrength(),
new EnemyFactory(EntityType.CHICKEN, 8, 0.5), new EnemyFactory(EntityType.CHICKEN, 8, 0.6).getStrength()
);
public WaveGenerator(Towerdefense game) {
this(game, 1);
}
public WaveGenerator(Towerdefense game, int startLevel) {
this.game = game;
this.currentLevel = startLevel - 1;
}
public void startNextWave() {
this.currentLevel += 1;
List<EnemyFactory> enemyList = this.chooseEnemies();
Collections.shuffle(enemyList);
int averageDelay = Math.min(20000 / enemyList.size(), 1500);
AtomicInteger delay = new AtomicInteger(0);
enemyList.forEach(enemy -> {
delay.addAndGet(averageDelay);
MinecraftServer.getSchedulerManager().scheduleTask(
() -> this.spawnEnemy(enemyList.removeFirst()),
TaskSchedule.millis(delay.get() + this.random.nextInt(averageDelay-200, averageDelay+200)),
TaskSchedule.stop()
);
});
}
private void spawnEnemy(EnemyFactory enemy) {
this.game.getInstances().forEach(instance -> instance.addEnemy(enemy.buildEntity()));
}
private int getCurrentLevelStrength() {
return (int) (0.5 * Math.pow(2, this.currentLevel));
}
private List<EnemyFactory> chooseEnemies() {
int strength = this.getCurrentLevelStrength();
final List<EnemyFactory> usedEnemies = new ArrayList<>();
while(strength > 0) {
int finalStrength = strength;
Map.Entry<EnemyFactory, Integer> chosenEnemy = this.availableEnemies.entrySet().stream()
.filter(e -> e.getValue() <= finalStrength)
.sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2))
.findAny()
.get();
usedEnemies.add(chosenEnemy.getKey());
strength -= chosenEnemy.getKey().getStrength();
}
return usedEnemies;
}
}

View File

@ -0,0 +1,16 @@
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 ShootingTower {
public BlazeTower() {
super(EntityType.BLAZE, 2, 2, 15);
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.shootProjectile(enemy, EntityType.FIREBALL, 2, 0);
}
}

View File

@ -0,0 +1,19 @@
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.item.ItemStack;
import net.minestom.server.item.Material;
public class BruteTower extends Tower {
public BruteTower() {
super(EntityType.PIGLIN_BRUTE, 20, 2, 4);
this.setItemInMainHand(ItemStack.of(Material.GOLDEN_AXE));
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.causeDamage(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;
public class EnderDragonTower extends ShootingTower{
public EnderDragonTower() {
super(EntityType.ENDER_DRAGON, 40, 7, 20);
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.DRAGON_FIREBALL, 2, 0);
}
@Override
protected void onProjectileHit(EntityCreature enemy) {
super.onProjectileHit(enemy);
}
}

View File

@ -0,0 +1,16 @@
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 EndermanTower extends Tower {
public EndermanTower() {
super(EntityType.ENDERMAN, 12, 1, 6);
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.causeDamage(enemy);
}
}

View File

@ -0,0 +1,63 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
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.EntityCreature;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
public abstract class ShootingTower extends Tower {
public ShootingTower(@NotNull EntityType entityType, int damage, double attacksPerSecond, int range) {
super(entityType, damage, attacksPerSecond, range);
}
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, 1));
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;
this.causeDamage(target);
this.onProjectileHit(target);
target.setFlyingWithElytra(true);
projectile.remove();
})
.addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove());
}
protected void onProjectileHit(EntityCreature enemy) {
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
}
}

View File

@ -0,0 +1,25 @@
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.item.ItemStack;
import net.minestom.server.item.Material;
public class SkeletonTower extends ShootingTower {
public SkeletonTower() {
super(EntityType.SKELETON, 1, 2, 10);
this.setItemInMainHand(ItemStack.of(Material.BOW));
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
}
@Override
protected void onProjectileHit(EntityCreature enemy) {
super.onProjectileHit(enemy);
// enemy.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue()*0.5);
}
}

View File

@ -0,0 +1,135 @@
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.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.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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 final static int damageDivider = 10;
private Priority priority = Priority.FIRST;
protected float damage;
protected int range;
protected double attacksPerSecond;
protected TaskSchedule attackDelay;
private @Nullable Task attackTask;
private float sellingPriceMultiplier = 1;
public Tower(@NotNull EntityType entityType, int damage, double attacksPerSecond, int range) {
super(entityType);
this.damage = (float) damage / damageDivider;
this.range = range;
this.attacksPerSecond = attacksPerSecond;
this.attackDelay = TaskSchedule.millis((long) (1000/attacksPerSecond));
}
public void startShooting() {
this.attackTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> {
EntityCreature nextEnemy = this.getNextEnemy();
if(nextEnemy == null) return this.attackDelay;
this.lookAt(nextEnemy);
this.attack(nextEnemy);
return this.attackDelay;
}, TaskSchedule.immediate());
}
@Override
protected void remove(boolean permanent) {
if(this.attackTask != null) this.attackTask.cancel();
super.remove(permanent);
}
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();
};
}
protected TowerdefenseRoom getRoomInstance() {
return (TowerdefenseRoom) this.getInstance();
}
public int getSellingPrice(int buyPrice) {
return (int) (this.sellingPriceMultiplier * buyPrice);
}
public double getValue() {
return this.damage * this.attacksPerSecond * this.range * 2;
}
public int getRange() {
return this.range;
}
protected void causeDamage(EntityCreature enemy) {
this.causeDamage(enemy, this.damage);
}
protected void causeDamage(EntityCreature enemy, float amount) {
enemy.damage(DamageType.PLAYER_ATTACK, amount);
if(enemy.isDead()) this.getRoomInstance().addMoney((int) enemy.getAttribute(Attribute.MAX_HEALTH).getBaseValue());
}
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;
public class WitherTower extends ShootingTower {
public WitherTower() {
super(EntityType.WITHER, 18, 4, 13);
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.WITHER_SKULL, 2, 0);
}
@Override
protected void onProjectileHit(EntityCreature enemy) {
super.onProjectileHit(enemy);
}
}

View File

@ -0,0 +1,19 @@
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.item.ItemStack;
import net.minestom.server.item.Material;
public class ZombieTower extends Tower {
public ZombieTower() {
super(EntityType.ZOMBIE, 7, 1, 4);
this.setItemInMainHand(ItemStack.of(Material.IRON_SWORD));
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.causeDamage(enemy);
}
}

View File

@ -15,6 +15,7 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import net.minestom.server.instance.anvil.AnvilLoader;
@ -66,6 +67,11 @@ public class Room extends MineNetInstance implements Spawnable {
p.getInventory().clear();
p.setGameMode(GameMode.ADVENTURE);
p.setInvisible(false);
p.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(p.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).attribute().defaultValue());
p.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(p.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).attribute().defaultValue());
p.heal();
p.setExp(0);
p.setLevel(0);
rooms.add(room);
players.put(p, room);
MoveInstance.move(p, room);