Author SHA1 Message Date
jannis 90395f71e9 Merge remote-tracking branch 'origin/develop-towerdefense' into develop-towerdefense 2026-11-09 21:37:08 +01:00
jannis b6c3f2c1e0 rebalance towerdefense: welcome title, spectator switcher, projectile/aim fixes, HP scaling 2026-11-09 21:35:28 +01:00
jannis e8e46c8e46 rebalance towerdefense: welcome title, spectator switcher, projectile/aim fixes, HP scaling 2026-11-09 21:28:07 +01:00
jannis d021f12ad0 Merge remote-tracking branch 'origin/develop' into develop-towerdefense 2026-11-09 20:49:53 +01:00
MineTec 29d347ec03 updated dependencies, replaced unreachable repo, and commented out unused libraries 2026-08-15 20:50:35 +02:00
Pupsi 460d737d3d reset player level 2025-07-19 02:21:23 +02:00
Pupsi 7591ce29ff added strong towers 2025-07-19 02:09:55 +02:00
Pupsi 39bbf19d9d reset more player values in Room 2025-07-19 01:48:35 +02:00
Pupsi df239d8166 made armadillo faster and chicken slower 2025-07-19 01:27:35 +02:00
Pupsi bf9c16ccc8 removed log 2025-07-19 01:24:40 +02:00
Pupsi b5d6d7bac4 added wave mechanic 2025-07-19 01:10:57 +02:00
Pupsi 3b55f16b24 added range to cursor 2025-04-16 23:43:46 +02:00
Pupsi 5ef254b75f fixed tower spawning issues 2025-04-14 17:26:59 +02:00
Pupsi 5e5f36153e added own cursor class 2025-04-13 23:28:46 +02:00
Pupsi e77879c657 added tower value calculation and delete tower mechanic 2025-04-13 18:35:50 +02:00
Pupsi e3068be160 moved shooting mechanic to ShootingTower, added money 2025-04-13 16:12:08 +02:00
Pupsi 867bee1c5a corrected PlayerLoginHandler 2025-04-13 13:07:28 +02:00
Pupsi 893923cc56 added towers and added tower spawn mechanics 2025-04-12 23:48:06 +02:00
Pupsi 38c944e6c1 added basic towerdefense logic 2025-04-12 19:22:37 +02:00
18 changed files with 1080 additions and 126 deletions

No files matched your search

-1
View File
@@ -1,4 +1,3 @@
//for jar packaging see https://github.com/Protonull/BasicMinestomServer/blob/master/build.gradle.kts
plugins {
Vendored
+82 -82
View File
@@ -1,82 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
@@ -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);
}
}
@@ -2,40 +2,159 @@ 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.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
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.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
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 final List<TowerdefenseRoom> instances = new ArrayList<>();
private final Set<Player> spectators = new HashSet<>();
private final WaveGenerator waveGenerator;
private Task waveTask;
public static final Component SWITCHER_NAME = Component.text("Spieler wechseln", NamedTextColor.AQUA);
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());
this.setGenerator(new MazeGenerator());
this.generateMaze();
this.waveGenerator = new WaveGenerator(this);
}
@Override
protected void checkAbandoned() {
this.scheduleNextTick((instance) -> {
boolean noAlive = this.instances.stream().allMatch(room -> room.getPlayers().isEmpty());
boolean noSpectators = this.spectators.stream().noneMatch(Player::isOnline);
if(noAlive && this.getPlayers().isEmpty() && noSpectators) {
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)).thenRun(() -> this.welcome(player));
});
this.waveTask = MinecraftServer.getSchedulerManager().scheduleTask(
this.waveGenerator::startNextWave,
TaskSchedule.seconds(8),
TaskSchedule.seconds(15)
);
}
private void welcome(Player player) {
player.showTitle(Title.title(
Component.text("Tower Defense", NamedTextColor.GOLD),
Component.text("Verteidige dich gegen die Mobs!", NamedTextColor.YELLOW),
Title.Times.times(Duration.ofMillis(400), Duration.ofSeconds(3), Duration.ofMillis(600))
));
player.playSound(Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 1f, 1f));
player.sendMessage(Component.text("Die erste Welle kommt in 8 Sekunden...", NamedTextColor.GRAY));
}
public List<TowerdefenseRoom> getAliveInstances() {
synchronized (this.instances) {
return new ArrayList<>(this.instances);
}
}
public void removeInstance(TowerdefenseRoom room) {
synchronized (this.instances) {
this.instances.remove(room);
}
}
public Set<Player> getSpectators() {
return this.spectators;
}
public void beginSpectating(Player player) {
this.spectators.add(player);
ItemStack switcher = ItemStack.of(Material.COMPASS).withCustomName(SWITCHER_NAME);
player.getInventory().clear();
player.getInventory().setItemStack(4, switcher);
player.sendMessage(Component.text("Du bist gestorben! Nutze den Kompass, um andere Spieler zu beobachten.", NamedTextColor.RED));
this.cycleSpectator(player);
}
public void cycleSpectator(Player player) {
List<TowerdefenseRoom> alive = this.getAliveInstances();
if(alive.isEmpty()) {
player.sendMessage(Component.text("Kein Spieler mehr am Leben.", NamedTextColor.GRAY));
return;
}
TowerdefenseRoom current = alive.stream()
.filter(r -> r == player.getInstance())
.findFirst().orElse(null);
int nextIdx = current == null ? 0 : (alive.indexOf(current) + 1) % alive.size();
TowerdefenseRoom target = alive.get(nextIdx);
player.setInstance(target, new Pos(0, 2, 0)).thenRun(() -> {
player.setGameMode(net.minestom.server.entity.GameMode.SPECTATOR);
player.sendActionBar(Component.text("Du beobachtest: " + target.getPlayer().getUsername(), NamedTextColor.AQUA));
});
}
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++) {
position = position.add(direction - 1, 0, direction % 2);
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);
}
@@ -48,13 +167,17 @@ public class Towerdefense extends StatelessGame {
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);
this.mazePath.add(position.add(0.5, 1, 0.5));
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));
}
public AbsoluteBlockBatch getMazeBatch() {
@@ -65,12 +188,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;
}
}
@@ -1,67 +1,290 @@
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.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.collision.Aerodynamics;
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.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;
private boolean dead = false;
public TowerdefenseRoom(Player player, Towerdefense game) {
this(UUID.randomUUID(), player, game);
}
// eindeutiger Dimension-Name pro Instanz, siehe MineNetInstance
private TowerdefenseRoom(UUID uuid, Player player, Towerdefense game) {
super(uuid, Dimension.OVERWORLD.key, net.kyori.adventure.key.Key.key("minenet", "instance-" + uuid));
super(UUID.randomUUID(), Dimension.OVERWORLD.key);
MinecraftServer.getInstanceManager().registerInstance(this);
this.player = player;
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);
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().entrySet().stream()
.sorted(java.util.Comparator.comparingInt(e -> this.game.getPrices().get(e.getValue())))
.forEach(entry -> {
int price = this.game.getPrices().get(entry.getValue());
this.player.getInventory().addItemStack(ItemStack.of(entry.getKey()).withMaxStackSize(price).withAmount(price));
});
this.setGenerator(new MazeGenerator());
BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {
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 -> {
if(event.getPlayer() != this.player || this.dead) return;
this.cursor.updateCursorPosition(this.player);
})
.addListener(PlayerUseItemEvent.class, event -> this.handleUse(event.getPlayer()))
.addListener(PlayerUseItemOnBlockEvent.class, event -> this.handleUse(event.getPlayer()))
.addListener(PlayerEntityInteractEvent.class, event -> this.handleUse(event.getPlayer()))
.addListener(PlayerHandAnimationEvent.class, event -> this.handleUse(event.getPlayer()))
.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);
});
}
private void handleUse(Player eventPlayer) {
if(this.game.getSpectators().contains(eventPlayer)) {
if(eventPlayer.getItemInMainHand().material() == Material.COMPASS) {
this.game.cycleSpectator(eventPlayer);
}
return;
}
if(eventPlayer != this.player || this.dead) return;
this.useItem();
}
private void useItem() {
Material itemInHand = this.player.getItemInMainHand().material();
if(itemInHand.equals(Material.BOW)) {
this.playerAttack();
return;
}
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.setNoGravity(true);
projectile.setAerodynamics(new Aerodynamics(0, 0.99, 0.99));
projectile.setView(p.getPosition().yaw(), p.getPosition().pitch());
Pos spawn = p.getPosition().add(0, p.getEyeHeight() - 0.2, 0);
projectile.scheduleRemove(Duration.ofSeconds(5));
projectile.eventNode()
.addListener(ProjectileCollideWithEntityEvent.class, hitEvent -> {
if(hitEvent.getTarget() == projectile.getShooter()) return;
try {
if(hitEvent.getTarget() instanceof EntityCreature target
&& !target.isDead()
&& this.enemies.contains(target)) {
target.damage(DamageType.PLAYER_ATTACK, 1.2f);
if(target.isDead()) this.addMoney(Math.max(1, (int) target.getAttribute(Attribute.MAX_HEALTH).getBaseValue() / 3));
}
} finally {
projectile.remove();
}
})
.addListener(ProjectileCollideWithBlockEvent.class, collisionEvent -> projectile.remove());
projectile.setInstance(this, spawn).thenRun(() -> {
Vec projectileVelocity = p.getPosition().direction().normalize().mul(60);
projectile.setVelocity(projectileVelocity);
});
}
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++;
}
public void addMoney(int amount) {
this.money += amount;
this.player.setLevel(this.money);
}
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, () -> this.changeEntityGoal(entity, 1));
public Material getPlayerHandMaterial() {
return this.player.getItemInMainHand().material();
}
private void changeEntityGoal(EntityCreature entity, int positionIndex) {
if(positionIndex == this.game.getMazePath().size() - 1) {
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;
}
entity.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex + 1), 0.7, () -> this.changeEntityGoal(entity, positionIndex + 1));
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() {
Tower tower = null;
Entity entity = this.player.getLineOfSightEntity(reach, e -> e instanceof Tower);
if(entity instanceof Tower t && this.towers.contains(t)) {
tower = t;
} else if(this.cursor.getTargetBlockPosition() != null) {
Pos target = new Pos(this.cursor.getTargetBlockPosition()).add(0.5, 0.5, 0.5);
tower = this.towers.stream()
.filter(t -> t.getPosition().distanceSquared(target) <= 4)
.min(java.util.Comparator.comparingDouble(t -> t.getPosition().distanceSquared(target)))
.orElse(null);
}
if(tower == null) 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();
if(this.dead) return;
float damage = (float) Math.ceil(enemy.getHealth()/10);
if(this.player.getHealth() - damage <= 0) {
this.handleDeath();
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));
}
private void handleDeath() {
this.dead = true;
this.game.removeInstance(this);
new ArrayList<>(this.enemies).forEach(Entity::remove);
this.enemies.clear();
new ArrayList<>(this.towers).forEach(Entity::remove);
this.towers.clear();
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.heal();
this.game.getScore().insertResult(this.player);
if(!this.game.getAliveInstances().isEmpty()) {
this.game.beginSpectating(this.player);
} else {
this.player.setInstance(this.game).thenRun(() -> MinecraftServer.getInstanceManager().unregisterInstance(this));
}
}
public boolean isDead() {
return this.dead;
}
public Towerdefense getGame() {
return this.game;
}
public List<EntityCreature> getEnemies() {
return this.enemies;
}
public Player getPlayer() {
return this.player;
}
}
@@ -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);
}
}
@@ -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));
}
}
}
@@ -0,0 +1,106 @@
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.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.timer.TaskSchedule;
import java.util.*;
import java.util.Optional;
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;
}
private static final int MAX_ENEMIES_PER_WAVE = 60;
private static final int MAX_ALIVE_PER_ROOM = 150;
public void startNextWave() {
this.currentLevel += 1;
if(this.game.getAliveInstances().isEmpty()) return;
List<EnemyFactory> enemyList = this.chooseEnemies();
if(enemyList.isEmpty()) return;
Collections.shuffle(enemyList);
int averageDelay = Math.min(20000 / enemyList.size(), 1500);
AtomicInteger delay = new AtomicInteger(0);
for (EnemyFactory enemy : enemyList) {
delay.addAndGet(averageDelay);
MinecraftServer.getSchedulerManager().scheduleTask(
() -> this.spawnEnemy(enemy),
TaskSchedule.millis(delay.get() + this.random.nextInt(averageDelay-200, averageDelay+200)),
TaskSchedule.stop()
);
}
}
private void spawnEnemy(EnemyFactory enemy) {
float hpMultiplier = this.getHealthMultiplier();
this.game.getAliveInstances().forEach(instance -> {
if(instance.isDead()) return;
if(instance.getEnemies().size() >= MAX_ALIVE_PER_ROOM) return;
try {
EntityCreature entity = enemy.buildEntity();
if(hpMultiplier > 1f) {
float scaledHp = (float) Math.min(1024, entity.getAttribute(net.minestom.server.entity.attribute.Attribute.MAX_HEALTH).getBaseValue() * hpMultiplier);
entity.getAttribute(net.minestom.server.entity.attribute.Attribute.MAX_HEALTH).setBaseValue(scaledHp);
entity.setHealth(scaledHp);
}
instance.addEnemy(entity);
} catch (Exception ignored) {
}
});
}
private float getHealthMultiplier() {
// Levels 1-3: unchanged. Then +12% HP per level, capped so tower
// damage still matters at very high levels.
return Math.min(5f, 1f + Math.max(0, this.currentLevel - 3) * 0.12f);
}
private int getCurrentLevelStrength() {
// Linear scaling (was 0.5 * 2^level — exponential blow-up).
int alive = Math.max(1, this.game.getAliveInstances().size());
return (int) (15 + this.currentLevel * 6 * Math.sqrt(alive));
}
private List<EnemyFactory> chooseEnemies() {
int strength = this.getCurrentLevelStrength();
final List<EnemyFactory> usedEnemies = new ArrayList<>();
while(strength > 0 && usedEnemies.size() < MAX_ENEMIES_PER_WAVE) {
int finalStrength = strength;
Optional<Map.Entry<EnemyFactory, Integer>> chosenEnemy = this.availableEnemies.entrySet().stream()
.filter(e -> e.getValue() <= finalStrength)
.sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2))
.findAny();
if(chosenEnemy.isEmpty()) break;
usedEnemies.add(chosenEnemy.get().getKey());
strength -= chosenEnemy.get().getKey().getStrength();
}
return usedEnemies;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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, 20, 3, 16);
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.DRAGON_FIREBALL, 2, 0);
}
@Override
protected void onProjectileHit(EntityCreature enemy) {
super.onProjectileHit(enemy);
}
}
@@ -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);
}
}
@@ -0,0 +1,69 @@
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() == projectile.getShooter()) return;
try {
if(event.getTarget() instanceof EntityCreature target
&& !target.isDead()
&& this.getRoomInstance().getEnemies().contains(target)) {
this.causeDamage(target);
this.onProjectileHit(target);
if(target.isDead()) target.setFlyingWithElytra(true);
}
} finally {
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);
}
}
@@ -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, 3, 2.5, 12);
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);
}
}
@@ -0,0 +1,151 @@
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 -> {
List<Pos> path = this.getRoomInstance().getGame().getMazePath();
yield enemyStream
.sorted((o1, o2) -> Integer.compare(
pathProgress(o1, path),
pathProgress(o2, path)
))
.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();
};
}
private static int pathProgress(EntityCreature enemy, List<Pos> path) {
// Highest index == furthest along the path. Returned as negative so a
// natural ascending sort puts the most-advanced enemy first.
double bestDist = Double.MAX_VALUE;
int bestIdx = 0;
for(int i = 0; i < path.size(); i++) {
double d = enemy.getPosition().distanceSquared(path.get(i));
if(d < bestDist) {
bestDist = d;
bestIdx = i;
}
}
return -bestIdx;
}
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(Math.max(1, (int) enemy.getAttribute(Attribute.MAX_HEALTH).getBaseValue() / 3));
}
protected abstract void attack(EntityCreature enemy);
}
@@ -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);
}
}
@@ -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);
}
}
+2 -1
View File
@@ -20,4 +20,5 @@ api:
admins:
- minetec
- 28Pupsi28
- 28Pupsi28
- jannis_mg