rebalance towerdefense: welcome title, spectator switcher, projectile/aim fixes, HP scaling

This commit is contained in:
jannis committed 2026-11-09 21:28:07 +01:00
1 parent d021f12ad0
commit e8e46c8e46
10 files changed
+313 -145

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 //for jar packaging see https://github.com/Protonull/BasicMinestomServer/blob/master/build.gradle.kts
plugins { plugins {
Vendored
+82 -82
View File
@@ -1,82 +1,82 @@
@rem @rem
@rem Copyright 2015 the original author or authors. @rem Copyright 2015 the original author or authors.
@rem @rem
@rem Licensed under the Apache License, Version 2.0 (the "License"); @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 not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem https://www.apache.org/licenses/LICENSE-2.0 @rem https://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @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 distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0 @rem SPDX-License-Identifier: Apache-2.0
@rem @rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem gradlew startup script for Windows @rem gradlew startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled @rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions setlocal EnableExtensions
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused @rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter. @rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 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. @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" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 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. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 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 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2 echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 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 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
@rem Execute gradlew @rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded @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 @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 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 :exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL% "%COMSPEC%" /c exit %ERRORLEVEL%
@@ -6,27 +6,38 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enem
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.*; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.*;
import eu.mhsl.minenet.minigames.score.LastWinsScore; 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.MinecraftServer;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.instance.batch.AbsoluteBlockBatch; import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material; import net.minestom.server.item.Material;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.Task; import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule; import net.minestom.server.timer.TaskSchedule;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set;
public class Towerdefense extends StatelessGame { public class Towerdefense extends StatelessGame {
private final Random random = new Random(); private final Random random = new Random();
private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch(); private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch();
private final List<Pos> mazePath = new ArrayList<>(); private final List<Pos> mazePath = new ArrayList<>();
private final List<TowerdefenseRoom> instances = new ArrayList<>(); private final List<TowerdefenseRoom> instances = new ArrayList<>();
private final Set<Player> spectators = new HashSet<>();
private final WaveGenerator waveGenerator; private final WaveGenerator waveGenerator;
private Task waveTask; 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( private final Map<Material, Class<? extends Tower>> availableTowers = Map.of(
Material.SKELETON_SPAWN_EGG, SkeletonTower.class, Material.SKELETON_SPAWN_EGG, SkeletonTower.class,
Material.ZOMBIE_SPAWN_EGG, ZombieTower.class, Material.ZOMBIE_SPAWN_EGG, ZombieTower.class,
@@ -59,7 +70,9 @@ public class Towerdefense extends StatelessGame {
@Override @Override
protected void checkAbandoned() { protected void checkAbandoned() {
this.scheduleNextTick((instance) -> { this.scheduleNextTick((instance) -> {
if(this.instances.stream().allMatch(room -> room.getPlayers().isEmpty()) && this.getPlayers().isEmpty()) { 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.waveTask.cancel();
this.unload(); this.unload();
} }
@@ -71,15 +84,67 @@ public class Towerdefense extends StatelessGame {
this.getPlayers().forEach(player -> { this.getPlayers().forEach(player -> {
TowerdefenseRoom newRoom = new TowerdefenseRoom(player, this); TowerdefenseRoom newRoom = new TowerdefenseRoom(player, this);
this.instances.add(newRoom); this.instances.add(newRoom);
player.setInstance(newRoom, new Pos(0, 1, 0)); player.setInstance(newRoom, new Pos(0, 1, 0)).thenRun(() -> this.welcome(player));
}); });
this.waveTask = MinecraftServer.getSchedulerManager().scheduleTask( this.waveTask = MinecraftServer.getSchedulerManager().scheduleTask(
this.waveGenerator::startNextWave, this.waveGenerator::startNextWave,
TaskSchedule.seconds(3), TaskSchedule.seconds(8),
TaskSchedule.seconds(25) 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() { private void generateMaze() {
Pos position = new Pos(0, 0, 0); Pos position = new Pos(0, 0, 0);
this.addMazePosition(position, Block.GREEN_WOOL); this.addMazePosition(position, Block.GREEN_WOOL);
@@ -10,6 +10,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.collision.Aerodynamics; import net.minestom.server.collision.Aerodynamics;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec; import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.*; import net.minestom.server.entity.*;
import net.minestom.server.entity.attribute.Attribute; import net.minestom.server.entity.attribute.Attribute;
@@ -42,6 +43,7 @@ public class TowerdefenseRoom extends InstanceContainer {
private final List<Tower> towers = new ArrayList<>(); private final List<Tower> towers = new ArrayList<>();
private final Cursor cursor; private final Cursor cursor;
private int money = 0; private int money = 0;
private boolean dead = false;
public TowerdefenseRoom(Player player, Towerdefense game) { public TowerdefenseRoom(Player player, Towerdefense game) {
super(UUID.randomUUID(), Dimension.OVERWORLD.key); super(UUID.randomUUID(), Dimension.OVERWORLD.key);
@@ -60,10 +62,12 @@ public class TowerdefenseRoom extends InstanceContainer {
this.player.getInventory().addItemStack( this.player.getInventory().addItemStack(
ItemStack.of(Material.BARRIER).withCustomName(Component.text("Löschen", TextColor.color(255,0,0))) ItemStack.of(Material.BARRIER).withCustomName(Component.text("Löschen", TextColor.color(255,0,0)))
); );
this.game.getAvailableTowers().forEach((material, tower) -> { this.game.getAvailableTowers().entrySet().stream()
int price = this.game.getPrices().get(tower); .sorted(java.util.Comparator.comparingInt(e -> this.game.getPrices().get(e.getValue())))
this.player.getInventory().addItemStack(ItemStack.of(material).withMaxStackSize(price).withAmount(price)); .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()); this.setGenerator(new MazeGenerator());
BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {}); BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {});
@@ -77,11 +81,14 @@ public class TowerdefenseRoom extends InstanceContainer {
if(!(event.getEntity() instanceof EntityCreature enemy)) return; if(!(event.getEntity() instanceof EntityCreature enemy)) return;
this.enemies.remove(enemy); this.enemies.remove(enemy);
}) })
.addListener(PlayerTickEvent.class, event -> this.cursor.updateCursorPosition(this.player)) .addListener(PlayerTickEvent.class, event -> {
.addListener(PlayerUseItemEvent.class, event -> this.useItem()) if(event.getPlayer() != this.player || this.dead) return;
.addListener(PlayerUseItemOnBlockEvent.class, event -> this.useItem()) this.cursor.updateCursorPosition(this.player);
.addListener(PlayerEntityInteractEvent.class, event -> this.useItem()) })
.addListener(PlayerHandAnimationEvent.class, event -> this.useItem()) .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(ItemDropEvent.class, CommonEventHandles::cancel)
.addListener(InventoryPreClickEvent.class, CommonEventHandles::cancel) .addListener(InventoryPreClickEvent.class, CommonEventHandles::cancel)
.addListener(PlayerSwapItemEvent.class, CommonEventHandles::cancel) .addListener(PlayerSwapItemEvent.class, CommonEventHandles::cancel)
@@ -90,6 +97,17 @@ public class TowerdefenseRoom extends InstanceContainer {
}); });
} }
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() { private void useItem() {
Material itemInHand = this.player.getItemInMainHand().material(); Material itemInHand = this.player.getItemInMainHand().material();
if(itemInHand.equals(Material.BOW)) { if(itemInHand.equals(Material.BOW)) {
@@ -108,23 +126,31 @@ public class TowerdefenseRoom extends InstanceContainer {
Player p = this.player; Player p = this.player;
this.lastPlayerAttack = System.currentTimeMillis(); this.lastPlayerAttack = System.currentTimeMillis();
EntityProjectile projectile = new EntityProjectile(p, EntityType.SPECTRAL_ARROW); EntityProjectile projectile = new EntityProjectile(p, EntityType.SPECTRAL_ARROW);
projectile.setView(p.getPosition().yaw(), p.getPosition().pitch());
projectile.setNoGravity(true); projectile.setNoGravity(true);
projectile.setAerodynamics(new Aerodynamics(0, 1, 1)); projectile.setAerodynamics(new Aerodynamics(0, 0.99, 0.99));
projectile.setView(p.getPosition().yaw(), p.getPosition().pitch());
Vec projectileVelocity = p.getPosition().direction().normalize().mul(20); Pos spawn = p.getPosition().add(0, p.getEyeHeight() - 0.2, 0);
projectile.setVelocity(projectileVelocity);
projectile.scheduleRemove(Duration.ofSeconds(5)); projectile.scheduleRemove(Duration.ofSeconds(5));
projectile.setInstance(this, p.getPosition().add(0, p.getEyeHeight()-0.5, 0));
projectile.eventNode() projectile.eventNode()
.addListener(ProjectileCollideWithEntityEvent.class, hitEvent -> { .addListener(ProjectileCollideWithEntityEvent.class, hitEvent -> {
if(!(hitEvent.getTarget() instanceof EntityCreature target)) return; if(hitEvent.getTarget() == projectile.getShooter()) return;
if(!this.getEnemies().stream().filter(entityCreature -> !entityCreature.isDead()).toList().contains(target)) return; try {
target.damage(DamageType.PLAYER_ATTACK, 0.1f); if(hitEvent.getTarget() instanceof EntityCreature target
if(target.isDead()) this.addMoney((int) target.getAttribute(Attribute.MAX_HEALTH).getBaseValue()); && !target.isDead()
projectile.remove(); && 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()); .addListener(ProjectileCollideWithBlockEvent.class, collisionEvent -> projectile.remove());
projectile.setInstance(this, spawn).thenRun(() -> {
Vec projectileVelocity = p.getPosition().direction().normalize().mul(60);
projectile.setVelocity(projectileVelocity);
});
} }
public void addMoney(int amount) { public void addMoney(int amount) {
@@ -163,9 +189,18 @@ public class TowerdefenseRoom extends InstanceContainer {
} }
private void removeTower() { private void removeTower() {
Entity entity = this.player.getLineOfSightEntity(reach, entity1 -> true); Tower tower = null;
if(!(entity instanceof Tower tower)) return; Entity entity = this.player.getLineOfSightEntity(reach, e -> e instanceof Tower);
if(!this.towers.contains(tower)) return; 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.setBlock(tower.getPosition().sub(0, 0.5, 0), Block.BLACK_WOOL);
this.addMoney(tower.getSellingPrice(this.game.getPrices().get(tower.getClass()))); this.addMoney(tower.getSellingPrice(this.game.getPrices().get(tower.getClass())));
@@ -207,15 +242,10 @@ public class TowerdefenseRoom extends InstanceContainer {
if(positionIndex == this.game.getMazePath().size()-1) { if(positionIndex == this.game.getMazePath().size()-1) {
this.enemies.remove(enemy); this.enemies.remove(enemy);
enemy.remove(); enemy.remove();
if(this.dead) return;
float damage = (float) Math.ceil(enemy.getHealth()/10); float damage = (float) Math.ceil(enemy.getHealth()/10);
if(this.player.getHealth() - damage <= 0) { if(this.player.getHealth() - damage <= 0) {
this.getEnemies().forEach(Entity::remove); this.handleDeath();
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; return;
} }
this.player.damage(DamageType.PLAYER_ATTACK, damage); this.player.damage(DamageType.PLAYER_ATTACK, damage);
@@ -224,6 +254,28 @@ public class TowerdefenseRoom extends InstanceContainer {
enemy.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.6, () -> this.changeEnemyGoal(enemy, positionIndex+1)); 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() { public Towerdefense getGame() {
return this.game; return this.game;
} }
@@ -2,10 +2,12 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.ene
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.Towerdefense; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.Towerdefense;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType; import net.minestom.server.entity.EntityType;
import net.minestom.server.timer.TaskSchedule; import net.minestom.server.timer.TaskSchedule;
import java.util.*; import java.util.*;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -32,44 +34,71 @@ public class WaveGenerator {
this.currentLevel = startLevel - 1; 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() { public void startNextWave() {
this.currentLevel += 1; this.currentLevel += 1;
if(this.game.getAliveInstances().isEmpty()) return;
List<EnemyFactory> enemyList = this.chooseEnemies(); List<EnemyFactory> enemyList = this.chooseEnemies();
if(enemyList.isEmpty()) return;
Collections.shuffle(enemyList); Collections.shuffle(enemyList);
int averageDelay = Math.min(20000 / enemyList.size(), 1500); int averageDelay = Math.min(20000 / enemyList.size(), 1500);
AtomicInteger delay = new AtomicInteger(0); AtomicInteger delay = new AtomicInteger(0);
enemyList.forEach(enemy -> { for (EnemyFactory enemy : enemyList) {
delay.addAndGet(averageDelay); delay.addAndGet(averageDelay);
MinecraftServer.getSchedulerManager().scheduleTask( MinecraftServer.getSchedulerManager().scheduleTask(
() -> this.spawnEnemy(enemyList.removeFirst()), () -> this.spawnEnemy(enemy),
TaskSchedule.millis(delay.get() + this.random.nextInt(averageDelay-200, averageDelay+200)), TaskSchedule.millis(delay.get() + this.random.nextInt(averageDelay-200, averageDelay+200)),
TaskSchedule.stop() TaskSchedule.stop()
); );
}); }
} }
private void spawnEnemy(EnemyFactory enemy) { private void spawnEnemy(EnemyFactory enemy) {
this.game.getInstances().forEach(instance -> instance.addEnemy(enemy.buildEntity())); 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() { private int getCurrentLevelStrength() {
return (int) (0.5 * Math.pow(2, this.currentLevel)); // 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() { private List<EnemyFactory> chooseEnemies() {
int strength = this.getCurrentLevelStrength(); int strength = this.getCurrentLevelStrength();
final List<EnemyFactory> usedEnemies = new ArrayList<>(); final List<EnemyFactory> usedEnemies = new ArrayList<>();
while(strength > 0) { while(strength > 0 && usedEnemies.size() < MAX_ENEMIES_PER_WAVE) {
int finalStrength = strength; int finalStrength = strength;
Map.Entry<EnemyFactory, Integer> chosenEnemy = this.availableEnemies.entrySet().stream() Optional<Map.Entry<EnemyFactory, Integer>> chosenEnemy = this.availableEnemies.entrySet().stream()
.filter(e -> e.getValue() <= finalStrength) .filter(e -> e.getValue() <= finalStrength)
.sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2)) .sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2))
.findAny() .findAny();
.get(); if(chosenEnemy.isEmpty()) break;
usedEnemies.add(chosenEnemy.getKey()); usedEnemies.add(chosenEnemy.get().getKey());
strength -= chosenEnemy.getKey().getStrength(); strength -= chosenEnemy.get().getKey().getStrength();
} }
return usedEnemies; return usedEnemies;
@@ -5,7 +5,7 @@ import net.minestom.server.entity.EntityType;
public class EnderDragonTower extends ShootingTower{ public class EnderDragonTower extends ShootingTower{
public EnderDragonTower() { public EnderDragonTower() {
super(EntityType.ENDER_DRAGON, 40, 7, 20); super(EntityType.ENDER_DRAGON, 20, 3, 16);
} }
@Override @Override
@@ -43,12 +43,18 @@ public abstract class ShootingTower extends Tower {
projectile.setInstance(this.getInstance(), startingPoint); projectile.setInstance(this.getInstance(), startingPoint);
projectile.eventNode() projectile.eventNode()
.addListener(ProjectileCollideWithEntityEvent.class, event -> { .addListener(ProjectileCollideWithEntityEvent.class, event -> {
if(!(event.getTarget() instanceof EntityCreature target)) return; if(event.getTarget() == projectile.getShooter()) return;
if(!this.getRoomInstance().getEnemies().contains(target)) return; try {
this.causeDamage(target); if(event.getTarget() instanceof EntityCreature target
this.onProjectileHit(target); && !target.isDead()
target.setFlyingWithElytra(true); && this.getRoomInstance().getEnemies().contains(target)) {
projectile.remove(); this.causeDamage(target);
this.onProjectileHit(target);
if(target.isDead()) target.setFlyingWithElytra(true);
}
} finally {
projectile.remove();
}
}) })
.addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove()); .addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove());
} }
@@ -7,7 +7,7 @@ import net.minestom.server.item.Material;
public class SkeletonTower extends ShootingTower { public class SkeletonTower extends ShootingTower {
public SkeletonTower() { public SkeletonTower() {
super(EntityType.SKELETON, 1, 2, 10); super(EntityType.SKELETON, 3, 2.5, 12);
this.setItemInMainHand(ItemStack.of(Material.BOW)); this.setItemInMainHand(ItemStack.of(Material.BOW));
} }
@@ -87,14 +87,15 @@ public abstract class Tower extends EntityCreature {
.filter(enemy -> !enemy.isDead()) .filter(enemy -> !enemy.isDead())
.filter(enemy -> enemy.getPosition().distance(this.getPosition()) <= this.range); .filter(enemy -> enemy.getPosition().distance(this.getPosition()) <= this.range);
return switch (this.priority.getType()) { return switch (this.priority.getType()) {
case PATH_DISTANCE -> enemyStream case PATH_DISTANCE -> {
.sorted((o1, o2) -> { List<Pos> path = this.getRoomInstance().getGame().getMazePath();
Pos endPoint = this.getRoomInstance().getGame().getMazePath().getLast(); yield enemyStream
return Double.compare( .sorted((o1, o2) -> Integer.compare(
o1.getPosition().distance(endPoint), pathProgress(o1, path),
o2.getPosition().distance(endPoint) pathProgress(o2, path)
); ))
}).toList(); .toList();
}
case TOWER_DISTANCE -> enemyStream case TOWER_DISTANCE -> enemyStream
.sorted((o1, o2) -> Double.compare( .sorted((o1, o2) -> Double.compare(
o1.getPosition().distance(this.getPosition()), o1.getPosition().distance(this.getPosition()),
@@ -106,6 +107,21 @@ public abstract class Tower extends EntityCreature {
}; };
} }
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() { protected TowerdefenseRoom getRoomInstance() {
return (TowerdefenseRoom) this.getInstance(); return (TowerdefenseRoom) this.getInstance();
} }
@@ -128,7 +144,7 @@ public abstract class Tower extends EntityCreature {
protected void causeDamage(EntityCreature enemy, float amount) { protected void causeDamage(EntityCreature enemy, float amount) {
enemy.damage(DamageType.PLAYER_ATTACK, amount); enemy.damage(DamageType.PLAYER_ATTACK, amount);
if(enemy.isDead()) this.getRoomInstance().addMoney((int) enemy.getAttribute(Attribute.MAX_HEALTH).getBaseValue()); if(enemy.isDead()) this.getRoomInstance().addMoney(Math.max(1, (int) enemy.getAttribute(Attribute.MAX_HEALTH).getBaseValue() / 3));
} }
protected abstract void attack(EntityCreature enemy); protected abstract void attack(EntityCreature enemy);
+2 -1
View File
@@ -20,4 +20,5 @@ api:
admins: admins:
- minetec - minetec
- 28Pupsi28 - 28Pupsi28
- jannis_mg