Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90395f71e9 | ||
|
|
b6c3f2c1e0 | ||
|
|
e8e46c8e46 | ||
|
|
d021f12ad0 | ||
|
|
29d347ec03 | ||
|
|
460d737d3d | ||
|
|
7591ce29ff | ||
|
|
39bbf19d9d | ||
|
|
df239d8166 | ||
|
|
bf9c16ccc8 | ||
|
|
b5d6d7bac4 | ||
|
|
3b55f16b24 | ||
|
|
5ef254b75f | ||
|
|
5e5f36153e | ||
|
|
e77879c657 | ||
|
|
e3068be160 | ||
|
|
867bee1c5a | ||
|
|
893923cc56 | ||
|
|
38c944e6c1 |
No files matched your search
@@ -1,4 +1,3 @@
|
||||
|
||||
//for jar packaging see https://github.com/Protonull/BasicMinestomServer/blob/master/build.gradle.kts
|
||||
|
||||
plugins {
|
||||
|
||||
Vendored
+82
-82
@@ -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%
|
||||
+89
@@ -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);
|
||||
}
|
||||
}
|
||||
+145
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
+252
-29
@@ -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;
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
+16
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
+20
@@ -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);
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+69
@@ -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);
|
||||
}
|
||||
}
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
+151
@@ -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);
|
||||
}
|
||||
+20
@@ -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);
|
||||
}
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -20,4 +20,5 @@ api:
|
||||
|
||||
admins:
|
||||
- minetec
|
||||
- 28Pupsi28
|
||||
- 28Pupsi28
|
||||
- jannis_mg
|
||||
Reference in new issue
Block a user