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 2d92eb7f0b Merge remote-tracking branch 'origin/develop-backrooms' into develop
# Conflicts:
#	src/main/resources/lang/locales.map.csv
2026-11-06 21:50:18 +01:00
MineTec 9e757378c1 added MobArena game mode with custom mobs and arena mechanics 2026-11-06 21:49:22 +01:00
MineTec dc32b053a3 Merge pull request 'Optionally drop chat locally when the proxy distributes it (velocity.chat-via-proxy)' (#18) from develop-jannis into develop
Reviewed-on: #18
2026-11-04 22:40:57 +00:00
MineTec f77d1290c7 Merge branch 'develop' into develop-jannis 2026-11-04 22:40:45 +00:00
MineTec d51ad387b4 Merge pull request 'fixed placement issues in BuildBattle and added isInArea methode to Position.java' (#16) from developBuildBattle into develop
Reviewed-on: #16
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-11-04 12:50:05 +00:00
MineTec 63afbaaf85 Merge branch 'develop' into developBuildBattle 2026-11-04 12:48:58 +00:00
Pupsi 1ef1d760ed fixed tetris game tick delay too low at high levels 2026-10-30 00:16:42 +01:00
MineTec 70be6418e0 Merge pull request 'Bug fixing' (#17) from bugFixing into develop
Reviewed-on: #17
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-10-28 21:13:04 +00:00
jannis 9d779a4879 fixed root cause of knockback issue in high ground 2026-10-28 22:07:45 +01:00
jannis 331efa9ffd replaced BlockPlacementHelper in BuildBattle with blocks-and-stuff placement rules 2026-10-28 21:56:28 +01:00
jannis f23495ecdd fixed knockback issue in high ground 2026-10-25 17:01:20 +01:00
jannis 89b81a3288 fixed spawning issues in tetris 2026-10-25 16:35:48 +01:00
jannis 7424d6b929 fixed placement issues in BuildBattle and added isInArea methode to Position.java 2026-10-25 00:18:01 +02:00
jannis a18a6b0dca fixed placement issues in BuildBattle and added isInArea methode to Position.java 2026-10-25 00:17:33 +02:00
MineTec 8dc165952a Optionally drop chat locally when the proxy distributes it (velocity.chat-via-proxy) 2026-10-17 22:47:11 +02:00
MineTec 3f1d2b86f2 Optionally drop chat locally when the proxy distributes it (velocity.chat-via-proxy) 2026-10-17 22:41:48 +02:00
jannis 4bd03c2603 Merge pull request 'added Build Battle' (#15) from develop-jannis into develop
Reviewed-on: #15
Reviewed-by: Lars Neuhaus <larslukasneuhaus@gmx.de>
2026-10-16 18:50:19 +00:00
jannis 75cd304bda fixed issues in BuildBattle 2026-10-16 20:43:14 +02:00
MineTec 53b13b6560 "Refactored BuildBattle game logic and chunk generator: optimized code structure, improved plot assignment, and enhanced rating system." 2026-10-15 01:25:59 +02:00
jannis 7e0c126a0d added BuildBattle 2026-10-14 23:00:56 +02:00
jannis 096b035503 fixed one issue in block battle 2026-08-21 09:22:19 +02:00
MineTec 29d347ec03 updated dependencies, replaced unreachable repo, and commented out unused libraries 2026-08-15 20:50:35 +02:00
jannis 103bd23bf4 fixed issues after ending in colorjump 2026-08-10 18:16:03 +02:00
jannis b175433701 fixed issues after ending in colorjump 2026-08-10 13:41: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
46 changed files with 2938 additions and 58 deletions

No files matched your search

+4 -1
View File
@@ -1,4 +1,3 @@
//for jar packaging see https://github.com/Protonull/BasicMinestomServer/blob/master/build.gradle.kts
plugins {
@@ -11,6 +10,7 @@ version '1.0-SNAPSHOT'
repositories {
mavenCentral()
maven { url = 'https://mvn.everbuild.org/public' }
}
java {
@@ -38,6 +38,9 @@ dependencies {
//PvP
implementation 'io.github.togar2:MinestomPvP:2026.05.30-26.1.1'
//Block placement
implementation 'org.everbuild.blocksandstuff:blocksandstuff-blocks:1.10.2-SNAPSHOT'
}
tasks {
@@ -1,5 +1,6 @@
package eu.mhsl.minenet.minigames.handler.global;
import eu.mhsl.minenet.minigames.Main;
import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import net.kyori.adventure.text.Component;
@@ -9,6 +10,9 @@ import net.minestom.server.event.player.PlayerChatEvent;
import org.jetbrains.annotations.NotNull;
public class ChatFormatHandler implements EventListener<PlayerChatEvent> {
// Verteilt der Proxy den Chat (chat-bridge bei CraftAttack), darf er hier nicht noch einmal erscheinen
private final boolean chatViaProxy = Main.globalConfig.node("velocity", "chat-via-proxy").getBoolean(false);
@Override
public @NotNull Class<PlayerChatEvent> eventType() {
return PlayerChatEvent.class;
@@ -16,6 +20,10 @@ public class ChatFormatHandler implements EventListener<PlayerChatEvent> {
@Override
public @NotNull Result run(@NotNull PlayerChatEvent event) {
if(this.chatViaProxy) {
event.setCancelled(true);
return Result.SUCCESS;
}
NamedTextColor color = event.getPlayer().getPermissionLevel() > 0 ? NamedTextColor.AQUA : NamedTextColor.GRAY;
event.setFormattedMessage(
new ChatMessage(Icon.CHAT)
@@ -9,6 +9,7 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBattle.Block
import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace.BlockBreakRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.boatRace.BoatRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef.BowSpleefFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle.BuildBattleFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.colorJump.ColorJumpFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube.DeathcubeFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.elytraRace.ElytraRaceFactory;
@@ -16,6 +17,7 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge.Fastbr
import eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround.HighGroundFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.jumpDive.JumpDiveFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArenaFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.pillars.PillarsFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.PlayerRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake.SpaceSnakeFactory;
@@ -54,7 +56,9 @@ public enum GameList {
PILLARS(new PillarsFactory(), GameType.PROTOTYPE),
BLOCKBATTLE(new BlockBattleFactory(), GameType.PVP),
COLORJUMP(new ColorJumpFactory(), GameType.JUMPNRUN),
PLAYERRACE(new PlayerRaceFactory(), GameType.JUMPNRUN);
PLAYERRACE(new PlayerRaceFactory(), GameType.JUMPNRUN),
BUILDBATTLE(new BuildBattleFactory(), GameType.PROTOTYPE),
MOBARENA(new MobArenaFactory(), GameType.PVE);
private final GameFactory factory;
private final GameType type;
@@ -43,7 +43,7 @@ public class BlockBattle extends StatelessGame {
);
this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> {
if(this.isBeforeBeginning) finalAttackEvent.setCancelled(true);
if(!this.isRunning) finalAttackEvent.setCancelled(true);
finalAttackEvent.setBaseDamage(0);
((Player) finalAttackEvent.getTarget()).setHealth(20);
});
@@ -0,0 +1,226 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.score.PointsWinScore;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.event.player.PlayerBlockInteractEvent;
import net.minestom.server.event.player.PlayerBlockPlaceEvent;
import net.minestom.server.event.player.PlayerUseItemEvent;
import net.minestom.server.event.player.PlayerUseItemOnBlockEvent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.IntStream;
public class BuildBattle extends StatelessGame {
private static final int height = 100;
private static final int plotHalf = BuildBattleChunkgenerator.inner / 2;
private static final int rowCount = 5;
private static final int ratingSeconds = 15;
private static final List<Material> ratingMaterials = List.of(
Material.RED_CONCRETE, Material.ORANGE_CONCRETE, Material.YELLOW_CONCRETE,
Material.LIME_CONCRETE, Material.GREEN_CONCRETE
);
private static final List<ItemStack> ratingItems = IntStream.range(0, ratingMaterials.size())
.mapToObj(i -> ItemStack.builder(ratingMaterials.get(i))
.customName(Component.text((i + 1) + " Punkte"))
.build())
.toList();
private static final List<String> themes = List.of(
"Burg", "Raumschiff", "Unterwasserwelt", "Pizzeria", "Spukhaus",
"Bauernhof", "Dschungel", "Zukunftsstadt", "Piratenschiff", "Vulkan",
"Freizeitpark", "Leuchtturm", "Wikingerdorf", "Weihnachtsmarkt",
"Schloss", "Strand", "Berghütte", "Bahnhof", "Zoo", "Schule",
"Zeitreise", "Ein Haus, das auf dem Kopf steht", "Eine Stadt auf dem Rücken eines Riesen",
"Wohnen in einer Schneekugel", "Alles ist aus Süßigkeiten", "Ein Schiff in der Luft",
"Das Museum der Zukunft", "Das letzte Haus vor dem Weltuntergang",
"Wenn Schweine fliegen könnten", "Das Gefängnis für Creeper", "Die Wohnung von Herobrine",
"Ein Dönerladen im Weltall", "Ein Spa für Enderman", "Der Käse-Palast",
"Mein Zimmer nach 3 Wochen ohne Aufräumen", "Die Hüpfburg des Todes"
);
private int spawnPosx = 0;
private int spawnPosz = 0;
private final Map<Player, Pos> spawnPositions = new WeakHashMap<>();
private final int seconds;
private final Map<Player, Integer> currentVotes = new HashMap<>();
private Player currentOwner = null;
public BuildBattle(int seconds) {
super(Dimension.THE_END.key, "BuildBattle", new PointsWinScore());
this.seconds = seconds;
this.setGenerator(new BuildBattleChunkgenerator(height));
BuildBattleBlockRules.register();
this.eventNode().addListener(PlayerUseItemEvent.class, this::onRatingItemUse);
this.eventNode().addListener(PlayerUseItemOnBlockEvent.class, e -> FluidFlowHelper.onBucketUse(e, p -> this.isInArea(p, e.getPlayer())));
this.eventNode().addListener(PlayerBlockInteractEvent.class, e -> {
if (this.isInArea(e.getBlockPosition(), e.getPlayer())) BuildBattleBlockRules.onInteract(e);
});
}
@Override
protected void onStart() {
String theme = themes.get(this.rnd.nextInt(themes.size()));
this.showTitle(Title.title(
Component.text("Thema", NamedTextColor.GOLD),
Component.text(theme, NamedTextColor.YELLOW)
));
this.sendMessage(Component.text("Euer Thema: ", NamedTextColor.GRAY)
.append(Component.text(theme, NamedTextColor.YELLOW)));
this.getPlayers().forEach(player -> player.setGameMode(GameMode.CREATIVE));
this.scheduler().scheduleTask(this::endBuildPhase, TaskSchedule.seconds(this.seconds), TaskSchedule.stop());
this.scheduleCountdown();
}
private void scheduleCountdown() {
int[] marks = {30, 10, 5, 4, 3, 2, 1};
for (int m : marks) {
if (this.seconds <= m) continue;
int remaining = m;
this.scheduler().scheduleTask(() -> {
NamedTextColor color = remaining <= 5 ? NamedTextColor.RED : (remaining <= 10 ? NamedTextColor.GOLD : NamedTextColor.YELLOW);
this.sendMessage(Component.text("Noch ", NamedTextColor.GRAY)
.append(Component.text(remaining, color))
.append(Component.text(" Sekunde" + (remaining == 1 ? "" : "n") + " Bauzeit!", NamedTextColor.GRAY)));
}, TaskSchedule.seconds(this.seconds - m), TaskSchedule.stop());
}
}
private void endBuildPhase() {
this.sendMessage(Component.text("Die Bauzeit ist beendet!"));
this.getPlayers().forEach(p -> {
p.setGameMode(GameMode.ADVENTURE);
p.setAllowFlying(true);
p.setFlying(true);
});
this.scheduler().scheduleTask(this::rating, TaskSchedule.seconds(3), TaskSchedule.stop());
}
@Override
protected boolean onPlayerJoin(Player player) {
Pos plotCenter = new Pos(
this.spawnPosx * BuildBattleChunkgenerator.period + plotHalf,
height,
this.spawnPosz * BuildBattleChunkgenerator.period + plotHalf
);
this.spawnPositions.put(player, plotCenter);
if(this.spawnPosx >= rowCount - 1) {
this.spawnPosx = 0;
this.spawnPosz++;
} else this.spawnPosx ++;
player.setGameMode(GameMode.ADVENTURE);
this.scheduler().scheduleNextTick(() -> player.teleport(plotCenter.add(0, 1.5, 0)));
return super.onPlayerJoin(player);
}
@Override
protected void onBlockBreak(@NotNull PlayerBlockBreakEvent event) {
if (!this.isInArea(event.getBlockPosition(), event.getPlayer())) {
event.setCancelled(true);
return;
}
FluidFlowHelper.onBreak(event);
}
@Override
protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent event) {
if (!this.isInArea(event.getBlockPosition(), event.getPlayer())) {
event.setCancelled(true);
return;
}
FluidFlowHelper.onPlace(event);
}
private Pos safeSpawn(Player owner) {
Pos center = this.spawnPositions.get(owner);
int topY = center.blockY();
for (int y = 300; y >= center.blockY(); y--) {
if (!this.getBlock(center.blockX(), y, center.blockZ()).isAir()) {
topY = y;
break;
}
}
return new Pos(center.x(), topY + 3, center.z()).withView(center.yaw(), center.pitch());
}
private void rating() {
Queue<Player> owners = new ArrayDeque<>(this.getPlayers());
this.scheduler().submitTask(() -> {
if (this.currentOwner != null) this.finishRound();
Player next = owners.poll();
while (next != null && !(next.isOnline() && this.spawnPositions.containsKey(next))) next = owners.poll();
if (next == null) return TaskSchedule.stop();
this.startRound(next);
return TaskSchedule.seconds(ratingSeconds);
});
}
private void startRound(Player owner) {
this.currentOwner = owner;
this.currentVotes.clear();
Pos plot = this.safeSpawn(owner);
this.sendMessage(Component.text("Bewerte das Grundstück von " + owner.getUsername() + "!"));
this.getPlayers().forEach(p -> {
p.teleport(plot);
p.getInventory().clear();
if (p != owner) this.giveRatingItems(p);
});
}
private void giveRatingItems(Player player) {
for (int i = 0; i < ratingItems.size(); i++) {
player.getInventory().setItemStack(i, ratingItems.get(i));
}
}
private void onRatingItemUse(PlayerUseItemEvent event) {
Player voter = event.getPlayer();
if (this.currentOwner == null || voter == this.currentOwner) return;
int slot = voter.getHeldSlot();
if (slot >= ratingItems.size()) return;
event.setCancelled(true);
this.currentVotes.put(voter, slot + 1);
voter.sendMessage(Component.text("Du hast " + (slot + 1) + " Punkte vergeben."));
}
private void finishRound() {
int voteSum = this.currentVotes.values().stream()
.mapToInt(Integer::intValue)
.sum();
int score = (int) Math.round((double) voteSum / this.getPlayers().size());
this.getPlayers().forEach(p -> p.getInventory().clear());
this.getScore().insertResult(this.currentOwner, score);
this.currentOwner = null;
}
private boolean isInArea(Point block, Player player) {
Pos center = this.spawnPositions.get(player);
if (center == null) return false;
Pos min = new Pos(center.x() - plotHalf, center.y() + 1, center.z() - plotHalf);
Pos max = new Pos(center.x() + plotHalf - 1, 300, center.z() + plotHalf - 1);
return eu.mhsl.minenet.minigames.util.Position.isInArea(block, min, max);
}
}
@@ -0,0 +1,85 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.player.PlayerBlockInteractEvent;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.instance.block.rule.BlockPlacementRule;
import org.everbuild.blocksandstuff.blocks.group.VanillaBlockBehaviour;
import org.everbuild.blocksandstuff.blocks.group.VanillaPlacementRules;
import org.everbuild.blocksandstuff.blocks.group.behaviour.BehaviourGroup;
import org.everbuild.blocksandstuff.blocks.group.placement.PlacementGroup;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
public final class BuildBattleBlockRules {
private BuildBattleBlockRules() {}
private static final ThreadLocal<Boolean> interactingInBuildBattle = ThreadLocal.withInitial(() -> false);
private static final Map<Integer, BlockHandler> behaviours = new HashMap<>();
private static boolean registered = false;
public static synchronized void register() {
if (registered) return;
registered = true;
var blockManager = MinecraftServer.getBlockManager();
for (PlacementGroup group : VanillaPlacementRules.INSTANCE.getALL()) {
for (Block block : group.getBlockGroup().allMatching()) {
blockManager.registerBlockPlacementRule(new BuildBattleOnlyRule(group.createRule(block)));
}
}
for (BehaviourGroup group : VanillaBlockBehaviour.INSTANCE.getALL()) {
for (Block block : group.getBlockGroup().allMatching()) {
behaviours.put(block.id(), group.createHandler(block));
}
}
MinecraftServer.getGlobalEventHandler().addListener(PlayerBlockInteractEvent.class,
e -> interactingInBuildBattle.set(e.getInstance() instanceof BuildBattle));
}
public static void onInteract(PlayerBlockInteractEvent event) {
BlockHandler handler = behaviours.get(event.getBlock().id());
if (handler == null) return;
boolean continueItemUse = handler.onInteract(new BlockHandler.Interaction(
event.getBlock(), event.getInstance(), event.getBlockFace(), event.getBlockPosition(),
event.getCursorPosition(), event.getPlayer(), event.getHand()
));
if (!continueItemUse) event.setBlockingItemUse(true);
}
private static final class BuildBattleOnlyRule extends BlockPlacementRule {
private final BlockPlacementRule delegate;
private BuildBattleOnlyRule(BlockPlacementRule delegate) {
super(delegate.getBlock());
this.delegate = delegate;
}
@Override
public @Nullable Block blockPlace(PlacementState placementState) {
if (!(placementState.instance() instanceof BuildBattle)) return placementState.block();
return this.delegate.blockPlace(placementState);
}
@Override
public Block blockUpdate(UpdateState updateState) {
if (!(updateState.instance() instanceof BuildBattle)) return updateState.currentBlock();
return this.delegate.blockUpdate(updateState);
}
@Override
public boolean isSelfReplaceable(Replacement replacement) {
return interactingInBuildBattle.get() && this.delegate.isSelfReplaceable(replacement);
}
@Override
public int maxUpdateDistance() {
return this.delegate.maxUpdateDistance();
}
}
}
@@ -0,0 +1,41 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle;
import eu.mhsl.minenet.minigames.util.GeneratorUtils;
import eu.mhsl.minenet.minigames.world.generator.terrain.BaseGenerator;
import net.minestom.server.instance.block.Block;
public class BuildBattleChunkgenerator extends BaseGenerator {
static final int inner = 32;
private static final int ring = 3;
private static final int wall = 1;
private static final int wallHeight = 200;
static final int period = wall + ring + inner + ring;
private static final int innerStart = wall + ring;
private static final int innerEnd = innerStart + inner;
private static final Block middleBlocks = Block.QUARTZ_BLOCK;
private static final Block outerBlocks = Block.STONE_BRICKS;
private static final Block wallBlocks = Block.POLISHED_DEEPSLATE;
public BuildBattleChunkgenerator(int height) {
this.addMixIn(unit -> GeneratorUtils.foreachXZ(unit, point -> {
int lx = Math.floorMod(point.blockX() + innerStart, period);
int lz = Math.floorMod(point.blockZ() + innerStart, period);
if (lx < wall || lz < wall) {
unit.modifier().fill(point.withY(height), point.add(1, 0, 1).withY(height + wallHeight + 1), wallBlocks);
} else {
unit.modifier().setBlock(point.withY(height), floorBlock(lx, lz));
}
}));
}
private static Block floorBlock(int lx, int lz) {
boolean inInner = lx >= innerStart && lx < innerEnd
&& lz >= innerStart && lz < innerEnd;
return inInner ? middleBlocks : outerBlocks;
}
}
@@ -0,0 +1,40 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle;
import eu.mhsl.minenet.minigames.instance.game.Game;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
import net.minestom.server.item.Material;
import java.util.Map;
public class BuildBattleFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_BuildBattle#name");
}
@Override
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("game_BuildBattle#seconds"), 60, 120, 300, 600));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
return new BuildBattle(configuration.get("seconds").getAsInt()).setParent(parent);
}
@Override
public Material symbol() {
return Material.SANDSTONE;
}
@Override
public TranslatedComponent description() {
return TranslatedComponent.byId("game_BuildBattle#description");
}
}
@@ -0,0 +1,267 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle;
import net.minestom.server.coordinate.BlockVec;
import net.minestom.server.coordinate.Point;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.event.player.PlayerBlockPlaceEvent;
import net.minestom.server.event.player.PlayerUseItemOnBlockEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Material;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.Direction;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
public final class FluidFlowHelper {
private FluidFlowHelper() {}
private static final int WATER_TICK = 5;
private static final int LAVA_TICK = 30;
private static final int WATER_RANGE = 7;
private static final int LAVA_RANGE = 3;
private static final int FALLING = 8;
public static void onBucketUse(PlayerUseItemOnBlockEvent event, Predicate<Point> inArea) {
Material m = event.getItemStack().material();
Instance inst = event.getPlayer().getInstance();
if (inst == null) return;
Point hit = event.getPosition();
var face = event.getBlockFace();
BlockVec placePos = new BlockVec(
hit.blockX() + face.toDirection().normalX(),
hit.blockY() + face.toDirection().normalY(),
hit.blockZ() + face.toDirection().normalZ()
);
if (!inArea.test(placePos)) return;
if (m == Material.WATER_BUCKET) {
inst.setBlock(placePos, Block.WATER.withProperty("level", "0"));
scheduleFlow(inst, placePos, true);
} else if (m == Material.LAVA_BUCKET) {
inst.setBlock(placePos, Block.LAVA.withProperty("level", "0"));
scheduleFlow(inst, placePos, false);
} else if (m == Material.BUCKET) {
Block existing = inst.getBlock(placePos);
if (isWater(existing) || isLava(existing)) inst.setBlock(placePos, Block.AIR);
}
}
public static void onPlace(PlayerBlockPlaceEvent event) {
Block b = event.getBlock();
if (isWater(b) || isLava(b)) {
Instance inst = event.getInstance();
BlockVec pos = event.getBlockPosition();
scheduleFlow(inst, pos, isWater(b));
}
triggerNeighbors(event.getInstance(), event.getBlockPosition());
}
public static void onBreak(PlayerBlockBreakEvent event) {
triggerNeighbors(event.getInstance(), event.getBlockPosition());
}
private static void triggerNeighbors(Instance inst, BlockVec pos) {
for (Direction d : Direction.values()) {
BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY() + d.normalY(), pos.blockZ() + d.normalZ());
Block nb = inst.getBlock(np);
if (isWater(nb) || isLava(nb)) scheduleFlow(inst, np, isWater(nb));
}
}
private static void scheduleFlow(Instance inst, BlockVec pos, boolean water) {
int delay = water ? WATER_TICK : LAVA_TICK;
inst.scheduler().buildTask(() -> step(inst, pos, water)).delay(TaskSchedule.tick(delay)).schedule();
}
private static void step(Instance inst, BlockVec pos, boolean water) {
Block self = inst.getBlock(pos);
if (water ? !isWater(self) : !isLava(self)) return;
int level = parseLevel(self);
boolean source = level == 0;
boolean falling = level >= FALLING;
int flowLevel = source ? 1 : (falling ? 1 : level + 1);
int maxRange = water ? WATER_RANGE : LAVA_RANGE;
if (!source && !falling) {
int newLevel = computeIncomingLevel(inst, pos, water, maxRange);
if (newLevel == -1) {
inst.setBlock(pos, Block.AIR);
triggerNeighbors(inst, pos);
return;
}
if (newLevel != level) {
inst.setBlock(pos, (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(newLevel)));
level = newLevel;
flowLevel = level + 1;
}
}
if (water && !source && countAdjacentSources(inst, pos, true) >= 2
&& hasSolidBelow(inst, pos)) {
inst.setBlock(pos, Block.WATER.withProperty("level", "0"));
scheduleFlow(inst, pos, true);
triggerNeighbors(inst, pos);
return;
}
BlockVec below = new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ());
Block belowBlock = inst.getBlock(below);
if (isReplaceable(belowBlock) || (water ? isWater(belowBlock) : isLava(belowBlock))) {
if (handleFluidCollision(inst, below, water)) {
triggerNeighbors(inst, below);
return;
}
Block target = (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(FALLING));
if (!belowBlock.compare(target) || !target.getProperty("level").equals(belowBlock.getProperty("level"))) {
if (!(isWater(belowBlock) && parseLevel(belowBlock) == 0)
&& !(isLava(belowBlock) && parseLevel(belowBlock) == 0)) {
inst.setBlock(below, target);
scheduleFlow(inst, below, water);
triggerNeighbors(inst, below);
}
}
if (!hasSolidBelow(inst, pos) && !source) return;
}
if (flowLevel > maxRange) return;
Set<Direction> targets = findFlowTargets(inst, pos, water, maxRange - (source ? 0 : level));
for (Direction d : Direction.HORIZONTAL) {
if (!targets.contains(d)) continue;
BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ());
Block nb = inst.getBlock(np);
if (handleFluidCollision(inst, np, water)) continue;
if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) continue;
int existingLevel = (water ? isWater(nb) : isLava(nb)) ? parseLevel(nb) : Integer.MAX_VALUE;
if (existingLevel == 0) continue;
if (flowLevel < existingLevel) {
inst.setBlock(np, (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(flowLevel)));
scheduleFlow(inst, np, water);
triggerNeighbors(inst, np);
}
}
}
private static int computeIncomingLevel(Instance inst, BlockVec pos, boolean water, int maxRange) {
int min = Integer.MAX_VALUE;
BlockVec above = new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ());
Block ab = inst.getBlock(above);
if ((water ? isWater(ab) : isLava(ab))) return FALLING;
for (Direction d : Direction.HORIZONTAL) {
BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ());
Block nb = inst.getBlock(np);
if (water ? isWater(nb) : isLava(nb)) {
int nl = parseLevel(nb);
if (nl >= FALLING) nl = 0;
if (nl < min) min = nl;
}
}
if (min == Integer.MAX_VALUE) return -1;
int result = min + 1;
if (result > maxRange) return -1;
return result;
}
private static Set<Direction> findFlowTargets(Instance inst, BlockVec pos, boolean water, int remaining) {
Set<Direction> result = new HashSet<>();
if (remaining <= 0) return result;
int bestDist = Integer.MAX_VALUE;
int[] dist = new int[4];
Direction[] dirs = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
for (int i = 0; i < 4; i++) {
BlockVec np = new BlockVec(pos.blockX() + dirs[i].normalX(), pos.blockY(), pos.blockZ() + dirs[i].normalZ());
Block nb = inst.getBlock(np);
if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) { dist[i] = Integer.MAX_VALUE; continue; }
dist[i] = bfsDownhill(inst, np, water, remaining);
if (dist[i] < bestDist) bestDist = dist[i];
}
if (bestDist == Integer.MAX_VALUE) {
for (int i = 0; i < 4; i++) {
if (dist[i] < Integer.MAX_VALUE) result.add(dirs[i]);
}
} else {
for (int i = 0; i < 4; i++) {
if (dist[i] == bestDist) result.add(dirs[i]);
}
}
return result;
}
private static int bfsDownhill(Instance inst, BlockVec start, boolean water, int remaining) {
ArrayDeque<int[]> queue = new ArrayDeque<>();
Set<Long> visited = new HashSet<>();
queue.add(new int[]{start.blockX(), start.blockY(), start.blockZ(), 0});
visited.add(key(start.blockX(), start.blockY(), start.blockZ()));
while (!queue.isEmpty()) {
int[] c = queue.poll();
BlockVec cp = new BlockVec(c[0], c[1], c[2]);
Block below = inst.getBlock(new BlockVec(c[0], c[1] - 1, c[2]));
if (isReplaceable(below) && !(water ? isWater(below) : isLava(below))) return c[3];
if (c[3] >= remaining) continue;
for (Direction d : Direction.HORIZONTAL) {
int nx = c[0] + d.normalX(), nz = c[2] + d.normalZ();
long k = key(nx, c[1], nz);
if (visited.contains(k)) continue;
Block nb = inst.getBlock(new BlockVec(nx, c[1], nz));
if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) continue;
visited.add(k);
queue.add(new int[]{nx, c[1], nz, c[3] + 1});
}
}
return Integer.MAX_VALUE;
}
private static long key(int x, int y, int z) {
return ((long) x & 0x3FFFFFFL) | (((long) z & 0x3FFFFFFL) << 26) | (((long) y & 0xFFFL) << 52);
}
private static int countAdjacentSources(Instance inst, BlockVec pos, boolean water) {
int c = 0;
for (Direction d : Direction.HORIZONTAL) {
Block nb = inst.getBlock(new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()));
if ((water ? isWater(nb) : isLava(nb)) && parseLevel(nb) == 0) c++;
}
return c;
}
private static boolean hasSolidBelow(Instance inst, BlockVec pos) {
Block below = inst.getBlock(new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ()));
return below.isSolid();
}
private static boolean handleFluidCollision(Instance inst, BlockVec pos, boolean incomingIsWater) {
Block target = inst.getBlock(pos);
if (incomingIsWater && isLava(target)) {
boolean lavaSource = parseLevel(target) == 0;
inst.setBlock(pos, lavaSource ? Block.OBSIDIAN : Block.COBBLESTONE);
return true;
}
if (!incomingIsWater && isWater(target)) {
inst.setBlock(pos, Block.STONE);
return true;
}
return false;
}
private static boolean isReplaceable(Block b) {
if (b.isAir()) return true;
String p = b.key().value();
return p.equals("short_grass") || p.equals("tall_grass") || p.equals("fern") || p.equals("large_fern")
|| p.equals("dead_bush") || p.equals("seagrass") || p.equals("tall_seagrass") || p.equals("vine")
|| p.equals("snow") || p.equals("fire") || p.equals("soul_fire");
}
private static boolean isWater(Block b) { return b.compare(Block.WATER); }
private static boolean isLava(Block b) { return b.compare(Block.LAVA); }
private static int parseLevel(Block b) {
String s = b.getProperty("level");
if (s == null) return 0;
try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; }
}
}
@@ -172,5 +172,9 @@ public class ColorJump extends StatelessGame {
this.getScore().insertResult(player);
player.setGameMode(GameMode.SPECTATOR);
}
if(!this.isBeforeBeginning && !this.isRunning) {
player.setGameMode(GameMode.SPECTATOR);
player.teleport(this.getSpawn());
}
}
}
@@ -9,6 +9,7 @@ import io.github.togar2.pvp.events.FinalAttackEvent;
import io.github.togar2.pvp.events.PrepareAttackEvent;
import io.github.togar2.pvp.feature.CombatFeatures;
import io.github.togar2.pvp.feature.knockback.KnockbackSettings;
import net.minestom.server.ServerFlag;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.instance.InstanceTickEvent;
@@ -51,15 +52,18 @@ class HighGround extends StatelessGame {
this.eventNode().addListener(
EntityKnockbackEvent.class,
entityKnockbackEvent -> entityKnockbackEvent.setSettings(
entityKnockbackEvent -> {
double tps = ServerFlag.SERVER_TICKS_PER_SECOND;
entityKnockbackEvent.setSettings(
KnockbackSettings.builder()
.horizontal(1.1)
.vertical(KnockbackSettings.DEFAULT.vertical())
.verticalLimit(KnockbackSettings.DEFAULT.verticalLimit())
.extraHorizontal(KnockbackSettings.DEFAULT.extraHorizontal())
.extraVertical(KnockbackSettings.DEFAULT.extraVertical())
.vertical(KnockbackSettings.DEFAULT.vertical() / tps)
.verticalLimit(KnockbackSettings.DEFAULT.verticalLimit() / tps)
.extraHorizontal(KnockbackSettings.DEFAULT.extraHorizontal() / tps)
.extraVertical(KnockbackSettings.DEFAULT.extraVertical() / tps)
.build()
)
);
}
);
this.eventNode().addListener(InstanceTickEvent.class, instanceTickEvent -> {
@@ -0,0 +1,18 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity;
/**
* Knockback in viewing direction of the attacker
*/
public record Knockback(double x, double z) {
public static Knockback of(Entity attacker) {
double yaw = Math.toRadians(attacker.getPosition().yaw());
return new Knockback(Math.sin(yaw), -Math.cos(yaw));
}
public void apply(LivingEntity victim, float strength) {
victim.takeKnockback(strength, this.x, this.z);
}
}
@@ -0,0 +1,342 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
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.mobArena.mob.ArenaMob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.MobType;
import eu.mhsl.minenet.minigames.message.type.ActionBarMessage;
import eu.mhsl.minenet.minigames.message.type.TitleMessage;
import eu.mhsl.minenet.minigames.score.PointsWinScore;
import eu.mhsl.minenet.minigames.util.BatchUtil;
import eu.mhsl.minenet.minigames.util.NumberUtil;
import eu.mhsl.minenet.minigames.world.BlockPallet;
import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.attribute.AttributeModifier;
import net.minestom.server.entity.attribute.AttributeOperation;
import net.minestom.server.entity.damage.Damage;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.item.PickupExperienceEvent;
import net.minestom.server.event.item.PickupItemEvent;
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.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
public class MobArena extends StatelessGame {
private static final int MAX_MOBS = 150;
private static final int SPAWN_INTERVAL_SECONDS = 2;
private static final float SWORD_DAMAGE = 6;
private static final double SWEEP_RANGE = 3;
private static final long ATTACK_COOLDOWN_MILLIS = 500;
private static final int WALL_HEIGHT = 3;
private static final Key SPEED_MODIFIER = Key.key("minenet", "mob_arena_speed");
private final int seconds;
private final int radius;
private final int difficulty;
private final int deathPenalty;
private final boolean powerUps;
private int elapsedSeconds = 0;
private final Map<Player, Integer> points = new WeakHashMap<>();
private final Map<Player, Long> lastAttack = new WeakHashMap<>();
private final Map<Player, Long> strengthUntil = new WeakHashMap<>();
private final Map<Player, Long> speedUntil = new WeakHashMap<>();
public MobArena(int seconds, int radius, int difficulty, int deathPenalty, boolean powerUps) {
super(Dimension.OVERWORLD.key, "MobArena", new PointsWinScore());
this.seconds = seconds;
this.radius = radius;
this.difficulty = difficulty;
this.deathPenalty = deathPenalty;
this.powerUps = powerUps;
this.setTime(18000);
this.defaultClock().pause();
this.setGenerator(
new CircularPlateTerrainGenerator(radius)
.setPlateHeight(1)
.setPlatePallet(BlockPallet.STONE)
);
this.eventNode()
.addListener(EntityAttackEvent.class, this::onPlayerAttack)
.addListener(EntityDamageEvent.class, this::onPlayerDamage)
.addListener(EntityDeathEvent.class, this::onMobDeath)
.addListener(PickupItemEvent.class, this::onPickupItem)
.addListener(PickupExperienceEvent.class, pickupExperienceEvent -> {
// Orbs sind nur Effekt, das Level zeigt die Punkte an
pickupExperienceEvent.setCancelled(true);
Knockback knockback = Knockback.of(pickupExperienceEvent.getPlayer());
pickupExperienceEvent.getExperienceOrb().setVelocity(new Vec(knockback.x(), 0, knockback.z()).mul(5));
});
}
@Override
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
// Mauer am Plattformrand, damit weder Spieler noch Mobs die Arena verlassen
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
int outer = this.radius + 2;
for(int x = -outer; x <= outer; x++) {
for(int z = -outer; z <= outer; z++) {
double distance = new Pos(x, 0, z).distance(Pos.ZERO);
if(distance <= this.radius || distance > this.radius + 1.5) continue;
for(int y = 0; y <= WALL_HEIGHT; y++) batch.setBlock(x, y, z, Block.STONE_BRICKS);
}
}
BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null));
}
@Override
protected void onStart() {
this.getPlayers().forEach(player -> {
this.points.put(player, 0);
player.setLevel(0);
player.getInventory().clear();
player.getInventory().addItemStack(ItemStack.of(Material.IRON_SWORD));
player.setHealth(this.maxHealth(player));
});
this.setTimeLimit(this.seconds);
this.scheduler().submitTask(() -> {
if(!this.isRunning) return TaskSchedule.stop();
int amount = (int) Math.ceil(this.getPlayers().size() * this.difficulty / 2d * this.escalation());
for(int i = 0; i < amount; i++) this.spawnMob(MobType.getRandom().create());
this.elapsedSeconds += SPAWN_INTERVAL_SECONDS;
return TaskSchedule.seconds(SPAWN_INTERVAL_SECONDS);
});
this.scheduler().submitTask(() -> {
if(!this.isRunning) return TaskSchedule.stop();
this.getPlayers().forEach(player -> this.heal(player, 1));
return TaskSchedule.seconds(3);
});
}
@Override
protected void onStop() {
this.points.forEach((player, points) -> this.getScore().insertResult(player, points));
}
private void onPlayerAttack(@NotNull EntityAttackEvent event) {
if(!(event.getEntity() instanceof Player attacker)) return;
if(!(event.getTarget() instanceof ArenaMob target)) return;
if(!this.isRunning) return;
long now = System.currentTimeMillis();
if(now - this.lastAttack.getOrDefault(attacker, 0L) < ATTACK_COOLDOWN_MILLIS) return;
this.lastAttack.put(attacker, now);
float damage = now < this.strengthUntil.getOrDefault(attacker, 0L) ? SWORD_DAMAGE * 2 : SWORD_DAMAGE;
Collection<Entity> nearby = this.getNearbyEntities(target.getPosition(), SWEEP_RANGE);
if(nearby.size() > 1)
ArenaMob.particle(this, Particle.SWEEP_ATTACK, target.getPosition().add(0, 1, 0), new Vec(0.5), (int) NumberUtil.map(nearby.size(), 1, 10, 1, 5));
Knockback knockback = Knockback.of(attacker);
for(Entity entity : nearby) {
if(!(entity instanceof ArenaMob mob) || mob.isDead()) continue;
double distance = mob.getDistance(target);
mob.damage(Damage.fromPlayer(attacker, (float) NumberUtil.map(distance, SWEEP_RANGE, 0, 0, damage)));
knockback.apply(mob, (float) NumberUtil.map(distance, SWEEP_RANGE, 0, 0.1, 0.5));
ArenaMob.particle(this, Particle.FALLING_OBSIDIAN_TEAR, mob.getPosition(), new Vec(knockback.x(), 0, knockback.z()), 10);
this.scheduler().scheduleNextTick(() -> {
if(mob.isDead()) knockback.apply(mob, 1f);
});
}
}
private void onPlayerDamage(@NotNull EntityDamageEvent event) {
if(!(event.getEntity() instanceof Player player)) return;
if(!this.isRunning) {
event.setCancelled(true);
return;
}
if(event.getDamage().getAmount() < player.getHealth()) return;
// Spieler sterben nicht wirklich, sondern verlieren Punkte und starten am Spawn neu
event.setCancelled(true);
this.addPoints(player, -this.deathPenalty);
player.teleport(this.getSpawn());
player.setHealth(this.maxHealth(player));
player.setInvulnerable(true);
player.scheduler().scheduleTask(() -> player.setInvulnerable(false), TaskSchedule.seconds(3), TaskSchedule.stop());
new TitleMessage(Duration.ofSeconds(1))
.subtitle(subtitleMessage -> subtitleMessage.appendStatic(Component.text("-" + this.deathPenalty, NamedTextColor.RED)))
.appendTranslated("game_MobArena#death")
.send(player);
}
private void onMobDeath(@NotNull EntityDeathEvent event) {
if(!(event.getEntity() instanceof ArenaMob mob)) return;
if(!this.isRunning) return;
if(mob.getLastDamageSource() == null) return;
if(!(mob.getLastDamageSource().getAttacker() instanceof Player killer)) return;
this.addPoints(killer, mob.getPoints());
this.heal(killer, 1);
if(this.powerUps && this.rnd.nextDouble() < mob.getDropChance()) this.dropPowerUp(mob.getPosition());
}
private void dropPowerUp(Pos pos) {
ItemEntity item = new ItemEntity(ItemStack.of(PowerUp.getRandom().getMaterial()).withGlowing(true));
item.setPickupDelay(Duration.ofMillis(500));
item.setInstance(this, pos.add(0, 0.5, 0));
item.setVelocity(new Vec(0, 4, 0));
item.scheduleRemove(Duration.ofSeconds(15));
}
private void onPickupItem(@NotNull PickupItemEvent event) {
if(!(event.getLivingEntity() instanceof Player player)) {
event.setCancelled(true);
return;
}
PowerUp.byMaterial(event.getItemStack().material()).ifPresentOrElse(powerUp -> {
this.applyPowerUp(player, powerUp);
player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.PLAYER, 0.5f, 1.5f));
new ActionBarMessage().appendTranslated(powerUp.getTranslationKey()).send(player);
}, () -> event.setCancelled(true));
}
private void applyPowerUp(Player player, PowerUp powerUp) {
long until = System.currentTimeMillis() + PowerUp.DURATION_SECONDS * 1000L;
switch(powerUp) {
case HEAL -> this.heal(player, 8);
case STRENGTH -> this.strengthUntil.put(player, until);
case SPEED -> {
this.speedUntil.put(player, until);
player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER);
player.getAttribute(Attribute.MOVEMENT_SPEED).addModifier(new AttributeModifier(SPEED_MODIFIER, 0.4, AttributeOperation.ADD_MULTIPLIED_BASE));
player.scheduler().scheduleTask(() -> {
// ein erneut aufgesammeltes Power-up verlaengert die Wirkung
if(System.currentTimeMillis() < this.speedUntil.getOrDefault(player, 0L)) return;
player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER);
}, TaskSchedule.seconds(PowerUp.DURATION_SECONDS), TaskSchedule.stop());
}
}
}
@Override
protected void onUnload() {
this.getPlayers().forEach(player -> player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER));
}
private void addPoints(Player player, int amount) {
int newPoints = Math.max(0, this.points.getOrDefault(player, 0) + amount);
this.points.put(player, newPoints);
player.setLevel(newPoints);
}
private float maxHealth(Player player) {
return (float) player.getAttributeValue(Attribute.MAX_HEALTH);
}
private void heal(Player player, float amount) {
player.setHealth(Math.min(this.maxHealth(player), player.getHealth() + amount));
}
public void spawnMob(ArenaMob mob) {
this.spawnMob(mob, this.randomSpawnPosition(), 0);
}
/**
* Only living arena mobs count towards the limits, orbs, items, projectiles and corpses are ignored
*/
public void spawnMob(ArenaMob mob, Pos pos, int area) {
if(!this.isRunning) return;
int players = this.getPlayers().size();
int maxMobs = (int) Math.min(MAX_MOBS, (16 + 8 * players) * this.difficulty / 2d * this.escalation());
long livingMobs = this.livingMobs(ArenaMob.class);
if(livingMobs >= maxMobs) return;
if(this.livingMobs(mob.getClass()) >= mob.maxAlive(players)) return;
Pos spawnPos = area > 0 ? pos.add(this.rnd.nextInt(-area, area), 0, this.rnd.nextInt(-area, area)) : pos;
mob.setInstance(this, spawnPos).thenRun(() -> ArenaMob.particle(this, Particle.POOF, spawnPos, new Vec(1), 3));
}
/**
* Grows over the course of the round, more and more mobs spawn the longer the game runs
*/
private double escalation() {
return Math.min(2.5, 1 + this.elapsedSeconds / 90d);
}
public float getDamageMultiplier() {
return switch(this.difficulty) {
case 1 -> 0.5f;
case 3 -> 1f;
default -> 0.75f;
};
}
private long livingMobs(Class<? extends ArenaMob> type) {
return this.getEntities().stream()
.filter(type::isInstance)
.filter(entity -> !((ArenaMob) entity).isDead())
.count();
}
/**
* Random position on the arena floor, preferably not right next to a player
*/
private Pos randomSpawnPosition() {
Pos pos = this.getSpawn();
for(int i = 0; i < 10; i++) {
double angle = this.rnd.nextDouble(2 * Math.PI);
double distance = this.rnd.nextDouble(this.radius - 3);
pos = this.getSpawn().add(Math.cos(angle) * distance, 0, Math.sin(angle) * distance);
Pos candidate = pos;
if(this.getPlayers().stream().noneMatch(player -> player.getPosition().distance(candidate) < 6)) break;
}
return pos;
}
public void explode(Pos pos, @Nullable Entity source, @Nullable Entity attacker, float damage, double range) {
this.sendGroupedPacket(new ParticlePacket(Particle.EXPLOSION, pos.x(), pos.y(), pos.z(), 1, 1, 1, 0, 5));
this.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.NEUTRAL, 0.5f, 1), pos.x(), pos.y(), pos.z());
for(Entity entity : this.getNearbyEntities(pos, range)) {
if(!(entity instanceof LivingEntity livingEntity) || livingEntity == source) continue;
float amount = (float) NumberUtil.map(entity.getDistance(pos), range, 0, 0, damage);
if(livingEntity instanceof Player) amount *= this.getDamageMultiplier();
livingEntity.damage(new Damage(DamageType.EXPLOSION, source, attacker, pos, amount));
}
}
@Override
public Pos getSpawn() {
return new Pos(0.5, 1, 0.5);
}
}
@@ -0,0 +1,51 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
import eu.mhsl.minenet.minigames.instance.game.Game;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.BoolOption;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
import net.minestom.server.item.Material;
import java.util.Map;
public class MobArenaFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_MobArena#name");
}
@Override
public TranslatedComponent description() {
return TranslatedComponent.byId("game_MobArena#description");
}
@Override
public Material symbol() {
return Material.ZOMBIE_HEAD;
}
@Override
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 120, 180, 300, 60))
.addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 30, 40, 50, 20))
.addOption(new NumericOption("difficulty", Material.SKELETON_SKULL, TranslatedComponent.byId("game_MobArena#difficulty"), 2, 3, 1))
.addOption(new NumericOption("deathPenalty", Material.TOTEM_OF_UNDYING, TranslatedComponent.byId("game_MobArena#deathPenalty"), 3, 5, 10, 0))
.addOption(new BoolOption("powerUps", Material.GOLDEN_APPLE, TranslatedComponent.byId("game_MobArena#powerUps")));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) {
return new MobArena(
configuration.get("seconds").getAsInt(),
configuration.get("radius").getAsInt(),
configuration.get("difficulty").getAsInt(),
configuration.get("deathPenalty").getAsInt(),
configuration.get("powerUps").getAsBoolean()
).setParent(parent);
}
}
@@ -0,0 +1,40 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena;
import net.minestom.server.item.Material;
import java.util.Arrays;
import java.util.Optional;
import java.util.Random;
public enum PowerUp {
HEAL(Material.GOLDEN_APPLE, "game_MobArena#powerUp_heal"),
STRENGTH(Material.BLAZE_POWDER, "game_MobArena#powerUp_strength"),
SPEED(Material.SUGAR, "game_MobArena#powerUp_speed");
public static final int DURATION_SECONDS = 10;
private static final Random RANDOM = new Random();
private final Material material;
private final String translationKey;
PowerUp(Material material, String translationKey) {
this.material = material;
this.translationKey = translationKey;
}
public Material getMaterial() {
return this.material;
}
public String getTranslationKey() {
return this.translationKey;
}
public static Optional<PowerUp> byMaterial(Material material) {
return Arrays.stream(values()).filter(powerUp -> powerUp.material == material).findFirst();
}
public static PowerUp getRandom() {
return values()[RANDOM.nextInt(values().length)];
}
}
@@ -0,0 +1,193 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.Knockback;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.component.DataComponents;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.ExperienceOrb;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.attribute.AttributeInstance;
import net.minestom.server.entity.ai.goal.MeleeAttackGoal;
import net.minestom.server.entity.ai.goal.RandomLookAroundGoal;
import net.minestom.server.entity.ai.goal.RandomStrollGoal;
import net.minestom.server.entity.ai.target.ClosestEntityTarget;
import net.minestom.server.entity.damage.Damage;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class ArenaMob extends EntityCreature {
private static final int MAX_ORBS = 8;
private static final int BLOCK_LENGTH = 6;
private static final List<String> CHARACTERS = List.of("", "▏", "▎", "▍", "▌", "▋", "▊", "▉");
private static final String FULL_BLOCK_CHAR = "█";
private float damage = 2;
private int experience = 10;
private int points = 1;
private double dropChance = 0.08;
public ArenaMob(@NotNull EntityType entityType) {
super(entityType);
this.set(DataComponents.CUSTOM_NAME, generateHealthBar(this.getMaxHealth(), this.getHealth()));
this.setCustomNameVisible(true);
this.setRemovalAnimationDelay(3500);
this.aiGroup();
this.eventNode()
.addListener(EntityAttackEvent.class, this::onAttack)
.addListener(EntityDamageEvent.class, this::onDamage)
.addListener(EntityDeathEvent.class, this::onDeath);
}
public static void particle(Instance instance, Particle particle, Point pos, Vec offset, int count) {
instance.sendGroupedPacket(new ParticlePacket(particle, pos.x(), pos.y(), pos.z(), (float) offset.x(), (float) offset.y(), (float) offset.z(), 0, count));
}
protected void particle(Particle particle, Point pos, float offset, int count) {
if(this.getInstance() == null) return;
particle(this.getInstance(), particle, pos, new Vec(offset), count);
}
protected ClosestEntityTarget playerTarget() {
return new ClosestEntityTarget(this, 32, entity -> entity instanceof Player player && player.getGameMode() != GameMode.SPECTATOR);
}
protected void aiGroup() {
this.addAIGroup(
List.of(
new MeleeAttackGoal(this, 1.2, 20, TimeUnit.SERVER_TICK),
new RandomLookAroundGoal(this, 30),
new RandomStrollGoal(this, 5)
),
List.of(this.playerTarget())
);
}
protected void onAttack(@NotNull EntityAttackEvent event) {
if(!(event.getTarget() instanceof Player target)) return;
this.particle(Particle.LANDING_LAVA, target.getPosition(), 0.5f, 10);
Knockback.of(this).apply(target, 0.2f);
target.damage(Damage.fromEntity(this, this.scaledDamage(this.damage)));
}
protected void onDamage(@NotNull EntityDamageEvent event) {
this.particle(Particle.DAMAGE_INDICATOR, this.getPosition(), 1, 5);
event.setSound(SoundEvent.BLOCK_AMETHYST_BLOCK_STEP);
}
protected void onDeath(@NotNull EntityDeathEvent event) {
Instance instance = this.getInstance();
this.particle(Particle.SOUL, this.getPosition(), 0.5f, 3);
this.setCustomNameVisible(false);
for(int i = 0; i < Math.min(this.experience, MAX_ORBS); i++) {
ExperienceOrb orb = new ExperienceOrb((short) 1);
orb.setNoGravity(true);
orb.setInstance(instance, this.getPosition().add(0, 0.1, 0));
orb.setVelocity(new Vec(this.random(-5, 5), this.random(1, 3), this.random(-5, 5)));
instance.scheduler().scheduleTask(() -> {
particle(instance, Particle.LAVA, orb.getPosition(), Vec.ZERO, 1);
orb.remove();
}, TaskSchedule.millis((long) this.random(1000, 5000)), TaskSchedule.stop());
}
this.scheduler().scheduleTask(() -> this.teleport(this.getPosition().sub(0, 0.1, 0)), TaskSchedule.seconds(3), TaskSchedule.stop());
}
private double random(double min, double max) {
return min + Math.random() * (max - min);
}
@Override
public void setHealth(float health) {
super.setHealth(health);
if(!this.isDead()) this.set(DataComponents.CUSTOM_NAME, generateHealthBar(this.getMaxHealth(), this.getHealth()));
}
/**
* Damage against players, scaled by the arena difficulty
*/
protected float scaledDamage(float damage) {
if(this.getInstance() instanceof MobArena arena) return damage * arena.getDamageMultiplier();
return damage;
}
protected void setMaxHealth(float maxHealth) {
this.getAttribute(Attribute.MAX_HEALTH).setBaseValue(maxHealth);
this.setHealth(maxHealth);
}
protected void multiplySpeed(double factor) {
AttributeInstance speed = this.getAttribute(Attribute.MOVEMENT_SPEED);
speed.setBaseValue(speed.getBaseValue() * factor);
}
/**
* Maximum amount of living mobs of this type in the arena
*/
public int maxAlive(int players) {
return 6 + 4 * players;
}
public double getDropChance() {
return this.dropChance;
}
public void setDropChance(double dropChance) {
this.dropChance = dropChance;
}
public float getMaxHealth() {
return (float) this.getAttributeValue(Attribute.MAX_HEALTH);
}
public void setDamage(float damage) {
this.damage = damage;
}
public void setExperience(int experience) {
this.experience = experience;
}
public int getPoints() {
return this.points;
}
public void setPoints(int points) {
this.points = points;
}
private static @NotNull Component generateHealthBar(float maxHealth, float currentHealth) {
// Converts the health percentage into a number from 0-{blockLength} -- only 0 if the mob's health is 0
final double charHealth = (currentHealth / maxHealth) * BLOCK_LENGTH;
return Component.text()
.append(Component.text(FULL_BLOCK_CHAR.repeat((int) Math.floor(charHealth)), NamedTextColor.DARK_RED))
.append(Component.text(
CHARACTERS.get((int) Math.round((charHealth - Math.floor(charHealth)) * (CHARACTERS.size() - 1))),
NamedTextColor.GOLD
))
.build();
}
}
@@ -0,0 +1,52 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob;
import io.github.togar2.pvp.entity.projectile.CustomEntityProjectile;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.damage.Damage;
import net.minestom.server.entity.damage.DamageType;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
/**
* Arrow shot by arena mobs, only hits players and flies through other mobs
*/
public class MobArrow extends CustomEntityProjectile {
private final float damage;
public MobArrow(@NotNull Entity shooter, float damage) {
super(shooter, EntityType.ARROW);
this.damage = damage;
}
/**
* Velocity has to be set before spawning, otherwise the client never receives it
*/
public void shootAt(Point target, double power, double spread) {
Entity shooter = this.getShooter();
if(shooter == null || shooter.getInstance() == null) return;
this.shootTo(target, power, spread);
this.setInstance(shooter.getInstance(), shooter.getPosition().add(0, shooter.getEyeHeight(), 0));
}
@Override
protected boolean canHit(Entity entity) {
return entity instanceof Player player && player.getGameMode() != GameMode.SPECTATOR;
}
@Override
public boolean onHit(Entity entity) {
if(entity instanceof Player player) player.damage(new Damage(DamageType.ARROW, this, this.getShooter(), null, this.damage));
return true;
}
@Override
public boolean onStuck() {
this.scheduleRemove(Duration.ofSeconds(2));
return false;
}
}
@@ -0,0 +1,47 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Creeper;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Evoker;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Pillager;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Skeleton;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Slime;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Spider;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Zombie;
import java.util.Arrays;
import java.util.Random;
import java.util.function.Supplier;
public enum MobType {
ZOMBIE(Zombie::new, 30),
SKELETON(Skeleton::new, 14),
SPIDER(Spider::new, 14),
PILLAGER(Pillager::new, 12),
CREEPER(Creeper::new, 10),
SLIME(Slime::new, 10),
EVOKER(Evoker::new, 6);
private static final Random RANDOM = new Random();
private static final int TOTAL_WEIGHT = Arrays.stream(values()).mapToInt(type -> type.weight).sum();
private final Supplier<ArenaMob> factory;
private final int weight;
MobType(Supplier<ArenaMob> factory, int weight) {
this.factory = factory;
this.weight = weight;
}
public ArenaMob create() {
return this.factory.get();
}
public static MobType getRandom() {
int roll = RANDOM.nextInt(TOTAL_WEIGHT);
for(MobType type : values()) {
roll -= type.weight;
if(roll < 0) return type;
}
return ZOMBIE;
}
}
@@ -0,0 +1,39 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.kyori.adventure.sound.Sound;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.metadata.monster.CreeperMeta;
import net.minestom.server.event.entity.EntityAttackEvent;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
public class Creeper extends ArenaMob {
private boolean fusing = false;
public Creeper() {
super(EntityType.CREEPER);
this.setMaxHealth(12);
this.setPoints(2);
}
/**
* Ignites instead of attacking and explodes shortly after, no matter if the target is still close
*/
@Override
protected void onAttack(@NotNull EntityAttackEvent event) {
if(this.fusing) return;
this.fusing = true;
((CreeperMeta) this.getEntityMeta()).setState(CreeperMeta.State.FUSE);
this.getInstance().playSound(Sound.sound(SoundEvent.ENTITY_CREEPER_PRIMED, Sound.Source.HOSTILE, 1, 1), this.getPosition());
this.scheduler().scheduleTask(() -> {
if(this.isDead() || this.isRemoved()) return;
if(this.getInstance() instanceof MobArena arena) arena.explode(this.getPosition(), this, null, 8, 4);
this.remove();
}, TaskSchedule.millis(1500), TaskSchedule.stop());
}
}
@@ -0,0 +1,121 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.kyori.adventure.sound.Sound;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.ai.GoalSelector;
import net.minestom.server.entity.ai.goal.RangedAttackGoal;
import net.minestom.server.entity.metadata.monster.raider.EvokerMeta;
import net.minestom.server.entity.metadata.monster.raider.SpellcasterIllagerMeta;
import net.minestom.server.event.entity.EntityDamageEvent;
import net.minestom.server.particle.Particle;
import net.minestom.server.sound.SoundEvent;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.time.Cooldown;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;
public class Evoker extends ArenaMob {
private boolean hasTotem;
public Evoker() {
super(EntityType.EVOKER);
this.hasTotem = Math.random() < 0.5;
this.setMaxHealth(14);
this.setPoints(3);
this.setDropChance(0.3);
}
@Override
protected void aiGroup() {
Random rnd = new Random();
int range = rnd.nextInt(3, 16);
// keeps distance to the target without actually shooting
RangedAttackGoal keepDistance = new RangedAttackGoal(this, Duration.ofDays(1), range, range, true, 0, 0);
keepDistance.setProjectileGenerator((shooter, to, power, spread) -> {});
this.addAIGroup(
List.of(
new ActionGoal(this, Duration.ofSeconds(25), target -> {
this.lookAt(target);
((EvokerMeta) this.getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.SUMMON_VEX);
this.scheduler().scheduleTask(() -> {
if(this.isDead()) return;
if(this.getInstance() instanceof MobArena arena) {
int amount = rnd.nextInt(1, 3);
for(int i = 0; i < amount; i++) arena.spawnMob(new Vex(), this.getPosition().add(0, 2, 0), 3);
}
((EvokerMeta) this.getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.NONE);
}, TaskSchedule.seconds(2), TaskSchedule.stop());
}),
keepDistance
),
List.of(this.playerTarget())
);
}
@Override
protected void onDamage(@NotNull EntityDamageEvent event) {
super.onDamage(event);
if(!this.hasTotem || event.getDamage().getAmount() < this.getHealth()) return;
event.setCancelled(true);
this.hasTotem = false;
this.particle(Particle.TOTEM_OF_UNDYING, this.getPosition(), 2, 60);
this.getInstance().playSound(Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.HOSTILE, 0.5f, 1), this.getPosition());
this.setHealth(6);
}
private static final class ActionGoal extends GoalSelector {
private final Duration cooldown;
private final Consumer<Entity> consumer;
private long lastAction;
public ActionGoal(@NotNull EntityCreature entityCreature, @NotNull Duration cooldown, Consumer<Entity> consumer) {
super(entityCreature);
this.cooldown = cooldown;
this.consumer = consumer;
}
@Override
public boolean shouldStart() {
Entity target = this.entityCreature.getTarget();
if(target == null) target = this.findTarget();
if(target == null) return false;
if(Cooldown.hasCooldown(System.currentTimeMillis(), this.lastAction, this.cooldown)) return false;
this.entityCreature.setTarget(target);
return true;
}
@Override
public void start() {
}
@Override
public void tick(long time) {
if(!Cooldown.hasCooldown(time, this.lastAction, this.cooldown) && this.entityCreature.getTarget() != null) {
this.lastAction = time;
this.consumer.accept(this.entityCreature.getTarget());
}
}
@Override
public boolean shouldEnd() {
final Entity target = this.entityCreature.getTarget();
return target == null || target.isRemoved() || Cooldown.hasCooldown(System.currentTimeMillis(), this.lastAction, this.cooldown);
}
@Override
public void end() {
}
}
}
@@ -0,0 +1,15 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public class Pillager extends ArenaMob {
public Pillager() {
super(EntityType.PILLAGER);
this.setItemInMainHand(ItemStack.of(Material.STONE_AXE));
this.setMaxHealth(12);
this.setDamage(3);
}
}
@@ -0,0 +1,141 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.MobArrow;
import net.kyori.adventure.sound.Sound;
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.ai.GoalSelector;
import net.minestom.server.entity.ai.goal.RandomStrollGoal;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.sound.SoundEvent;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Skeleton extends ArenaMob {
private static final float ARROW_DAMAGE = 2;
public Skeleton() {
super(EntityType.SKELETON);
this.setItemInMainHand(ItemStack.of(Material.BOW));
this.setMaxHealth(10);
this.multiplySpeed(0.6);
this.setPoints(2);
}
@Override
protected void aiGroup() {
this.addAIGroup(
List.of(new ArcherGoal(this), new RandomStrollGoal(this, 5)),
List.of(this.playerTarget())
);
}
@Override
public int maxAlive(int players) {
return 3 + 2 * players;
}
private void shoot(Entity target) {
if(this.getInstance() == null) return;
Pos to = target.getPosition().add(0, target.getEyeHeight() * 0.7, 0);
new MobArrow(this, this.scaledDamage(ARROW_DAMAGE)).shootAt(to, 1.6, 4);
this.getInstance().playSound(Sound.sound(SoundEvent.ENTITY_SKELETON_SHOOT, Sound.Source.HOSTILE, 1, 1), this.getPosition());
}
/**
* Keeps a medium distance to the target, backs off when it gets too close and shoots in fixed intervals
*/
private static final class ArcherGoal extends GoalSelector {
private static final double MIN_DISTANCE = 5;
private static final double MAX_DISTANCE = 12;
private static final double SHOOT_RANGE = 18;
private static final long DRAW_MILLIS = 1000;
private static final long COOLDOWN_MILLIS = 2500;
private final Skeleton skeleton;
private long drawStart = 0;
private long lastShot = 0;
private long lastPathUpdate = 0;
private boolean retreating = false;
ArcherGoal(@NotNull Skeleton skeleton) {
super(skeleton);
this.skeleton = skeleton;
}
@Override
public boolean shouldStart() {
return this.findTarget() != null;
}
@Override
public void start() {
}
@Override
public void tick(long time) {
Entity target = this.findTarget();
if(target == null) return;
this.entityCreature.setTarget(target);
double distance = this.entityCreature.getDistance(target);
this.updateMovement(target, distance, time);
this.entityCreature.lookAt(target);
// beim Zurückweichen wird nicht geschossen
if(this.retreating || distance > SHOOT_RANGE) {
this.stopDrawing();
return;
}
if(this.drawStart == 0) {
if(time - this.lastShot < COOLDOWN_MILLIS) return;
this.drawStart = time;
this.skeleton.refreshActiveHand(true, false, false);
} else if(time - this.drawStart >= DRAW_MILLIS) {
this.skeleton.shoot(target);
this.lastShot = time;
this.stopDrawing();
}
}
private void updateMovement(Entity target, double distance, long time) {
if(time - this.lastPathUpdate < 500) return;
this.lastPathUpdate = time;
Pos position = this.entityCreature.getPosition();
this.retreating = distance < MIN_DISTANCE;
if(distance > MAX_DISTANCE) {
this.entityCreature.getNavigator().setPathTo(target.getPosition());
} else if(this.retreating) {
Vec away = position.sub(target.getPosition()).asVec().withY(0);
if(away.isZero()) away = new Vec(1, 0, 0);
this.entityCreature.getNavigator().setPathTo(position.add(away.normalize().mul(4)));
} else {
this.entityCreature.getNavigator().setPathTo(null);
}
}
private void stopDrawing() {
if(this.drawStart == 0) return;
this.drawStart = 0;
this.skeleton.refreshActiveHand(false, false, false);
}
@Override
public boolean shouldEnd() {
return this.findTarget() == null;
}
@Override
public void end() {
this.stopDrawing();
this.entityCreature.getNavigator().setPathTo(null);
}
}
}
@@ -0,0 +1,34 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.metadata.cube.SlimeMeta;
import net.minestom.server.event.entity.EntityDeathEvent;
import org.jetbrains.annotations.NotNull;
public class Slime extends ArenaMob {
private final int size;
public Slime() {
this(Math.random() < 0.25 ? 3 : 2);
}
public Slime(int size) {
super(EntityType.SLIME);
this.size = size;
((SlimeMeta) this.getEntityMeta()).setSize(size);
this.setMaxHealth(size * size);
this.setDamage(size);
this.setExperience(size * 2);
}
@Override
protected void onDeath(@NotNull EntityDeathEvent event) {
super.onDeath(event);
if(this.size <= 1 || !(this.getInstance() instanceof MobArena arena)) return;
for(int i = 0; i < 2; i++) arena.spawnMob(new Slime(this.size - 1), this.getPosition(), 1);
}
}
@@ -0,0 +1,12 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
public class Spider extends ArenaMob {
public Spider() {
super(EntityType.SPIDER);
this.setMaxHealth(4);
this.setDamage(1);
}
}
@@ -0,0 +1,32 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.EntityType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.particle.Particle;
import net.minestom.server.timer.TaskSchedule;
public class Vex extends ArenaMob {
private static final int LIFETIME_SECONDS = 12;
public Vex() {
super(EntityType.VEX);
this.setItemInMainHand(ItemStack.of(Material.GOLDEN_SWORD));
this.setMaxHealth(4);
this.setDamage(1);
this.setExperience(2);
this.setDropChance(0);
this.scheduler().scheduleTask(() -> {
if(this.isDead() || this.isRemoved()) return;
this.particle(Particle.POOF, this.getPosition(), 0.3f, 5);
this.remove();
}, TaskSchedule.seconds(LIFETIME_SECONDS), TaskSchedule.stop());
}
@Override
public int maxAlive(int players) {
return 2 + players / 2;
}
}
@@ -0,0 +1,75 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.entity.metadata.monster.zombie.ZombieMeta;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
public class Zombie extends ArenaMob {
private enum Variant {
NORMAL, BABY, ARMORED, EXPLODER, EXPERIENCE;
static Variant random() {
double roll = Math.random();
if(roll < 0.10) return EXPLODER;
if(roll < 0.18) return EXPERIENCE;
if(roll < 0.33) return BABY;
if(roll < 0.45) return ARMORED;
return NORMAL;
}
}
private final Variant variant = Variant.random();
public Zombie() {
super(EntityType.ZOMBIE);
this.setMaxHealth(14);
switch(this.variant) {
case BABY -> {
((ZombieMeta) this.getEntityMeta()).setBaby(true);
this.setMaxHealth(6);
this.multiplySpeed(1.4);
this.setDamage(1);
this.setPoints(2);
}
case ARMORED -> {
this.setEquipment(EquipmentSlot.HELMET, ItemStack.of(Material.IRON_HELMET));
this.setEquipment(EquipmentSlot.CHESTPLATE, ItemStack.of(Material.IRON_CHESTPLATE));
this.setItemInMainHand(ItemStack.of(Material.IRON_SWORD));
this.setMaxHealth(24);
this.setDamage(3);
this.multiplySpeed(0.8);
this.setPoints(3);
this.setDropChance(0.25);
}
case EXPLODER -> {
this.setItemInMainHand(ItemStack.of(Material.TNT));
this.setItemInOffHand(ItemStack.of(Material.FLINT_AND_STEEL));
}
case EXPERIENCE -> {
this.setItemInMainHand(ItemStack.of(Material.EXPERIENCE_BOTTLE));
this.setExperience(50);
this.setPoints(3);
this.setDropChance(0.5);
}
case NORMAL -> {
}
}
}
@Override
protected void onDeath(@NotNull EntityDeathEvent event) {
super.onDeath(event);
if(this.variant == Variant.EXPLODER && this.getInstance() instanceof MobArena arena) {
Entity killer = this.getLastDamageSource() != null ? this.getLastDamageSource().getAttacker() : null;
arena.explode(this.getPosition(), this, killer, 7, 3);
}
}
}
@@ -147,10 +147,10 @@ class Tetris extends StatelessGame {
TetrisGame tetrisGame = this.tetrisGames.get(p);
p.teleport(tetrisGame.getPlayerSpawnPosition());
tetrisGame.sidebar.addViewer(p);
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
p.teleport(tetrisGame.getPlayerSpawnPosition());
Entity ghostBlock = new Entity(EntityType.BLOCK_DISPLAY);
((BlockDisplayMeta) ghostBlock.getEntityMeta()).setBlockState(Block.AIR);
ghostBlock.setNoGravity(true);
@@ -159,7 +159,6 @@ class Tetris extends StatelessGame {
return TaskSchedule.stop();
}, TaskSchedule.nextTick());
return super.onPlayerJoin(p);
}
@@ -92,7 +92,7 @@ public class TetrisGame {
int standardTickDelay = 40;
if(this.isFast) standardTickDelay = 20;
TaskSchedule nextTick = TaskSchedule.tick(Math.round((float) standardTickDelay / this.level));
TaskSchedule nextTick = TaskSchedule.tick(Math.max(Math.round((float) standardTickDelay / this.level), 1));
if(this.paused) return nextTick;
this.tick();
return nextTick;
@@ -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);
});
}
public void startWave(List<EntityType> entities) {
int counter = 0;
for(EntityType entityType : entities) {
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
this.addEntity(new EntityCreature(entityType));
return TaskSchedule.stop();
}, TaskSchedule.millis(800L * counter));
counter++;
private void handleUse(Player eventPlayer) {
if(this.game.getSpectators().contains(eventPlayer)) {
if(eventPlayer.getItemInMainHand().material() == Material.COMPASS) {
this.game.cycleSpectator(eventPlayer);
}
}
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));
}
private void changeEntityGoal(EntityCreature entity, int positionIndex) {
if(positionIndex == this.game.getMazePath().size() - 1) {
return;
}
entity.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex + 1), 0.7, () -> this.changeEntityGoal(entity, positionIndex + 1));
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 addMoney(int amount) {
this.money += amount;
this.player.setLevel(this.money);
}
public Material getPlayerHandMaterial() {
return this.player.getItemInMainHand().material();
}
private synchronized void placeTower() {
if(!this.canPlaceActiveTower()) {
if(this.cursor.isCursorActive() && !this.enoughMoneyForActiveTower()) {
this.player.sendActionBar(Component.text("Nicht genug Geld!", TextColor.color(255,0,0)));
}
return;
}
try {
Material usedMaterial = this.player.getItemInMainHand().material();
Tower tower = this.game.getAvailableTowers().entrySet().stream()
.filter(materialClassEntry -> materialClassEntry.getKey().equals(usedMaterial))
.findFirst()
.orElseThrow()
.getValue()
.getConstructor()
.newInstance();
this.setBlock(this.cursor.getTargetBlockPosition(), Block.BLUE_WOOL);
tower.setInstance(this, this.cursor.getPosition());
this.towers.add(tower);
tower.startShooting();
this.addMoney(-this.game.getPrices().get(tower.getClass()));
} catch (Exception ignored) {
}
this.cursor.updateCursorPosition(this.player);
}
private void removeTower() {
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);
}
}
@@ -44,4 +44,17 @@ public class Position {
public static boolean hasPositionChanged(Pos oldPos, Pos newPos) {
return !oldPos.withView(0, 0).equals(newPos.withView(0, 0));
}
public static boolean isInArea(Point point, Point pos1, Point pos2) {
double minX = Math.min(pos1.x(), pos2.x());
double maxX = Math.max(pos1.x(), pos2.x());
double minY = Math.min(pos1.y(), pos2.y());
double maxY = Math.max(pos1.y(), pos2.y());
double minZ = Math.min(pos1.z(), pos2.z());
double maxZ = Math.max(pos1.z(), pos2.z());
return point.x() >= minX && point.x() <= maxX
&& point.y() >= minY && point.y() <= maxY
&& point.z() >= minZ && point.z() <= maxZ;
}
}
+3
View File
@@ -9,6 +9,8 @@ bungeecord:
velocity:
enabled: false
# Chat nur über den Proxy verteilen, hier verwerfen (chat-bridge)
chat-via-proxy: false
secret: ''
api:
@@ -19,3 +21,4 @@ api:
admins:
- minetec
- 28Pupsi28
- jannis_mg
+18 -1
View File
@@ -190,7 +190,24 @@ description;Jump on the right color!;Springe auf die richtige Farbe!
;;
ns:game_PlayerRace#;;
name;Player Race;Wettrennen
description;Get to the other side as quickly as possible!;Komme so schnell wie möglich auf die andere Seite!;;
description;Get to the other side as quickly as possible!;Komme so schnell wie möglich auf die andere Seite!
;;
ns:game_BuildBattle#;;
name;Build Battle;Bauwettbewerb
description;Build the best creation for the given theme before time runs out and win through the other players’ ratings.;Baue in der Zeit das beste Bauwerk zum Thema und gewinne durch die Bewertung der anderen Spieler.
seconds;Build Time;Bauzeit
;;
ns:game_MobArena#;;
name;Mob Arena;Mob-Arena
description;Fight hordes of monsters with a sweeping sword and collect points for every kill. Dying costs points!;Kämpfe mit deinem Schwert gegen Horden von Monstern und sammle Punkte für jeden Kill. Sterben kostet Punkte!
death;You died!;Du bist gestorben!
difficulty;Difficulty;Schwierigkeit
deathPenalty;Points lost on death;Punktabzug bei Tod
powerUps;Power-ups;Power-ups
powerUp_heal;Healed!;Geheilt!
powerUp_strength;Double damage for 10 seconds!;Doppelter Schaden für 10 Sekunden!
powerUp_speed;Speed for 10 seconds!;Schneller für 10 Sekunden!
;;
ns:game_Backrooms#;;
name;Backrooms;Backrooms
description;Escape the Backrooms before the others do - or be the last one alive!;Entkomme den Backrooms vor allen anderen - oder überlebe als Letzter!
1 map en_us de_de
190
191 ns:game_PlayerRace#
192 name Player Race Wettrennen
193 description Get to the other side as quickly as possible! Komme so schnell wie möglich auf die andere Seite!
194
195 ns:game_BuildBattle#
196 name Build Battle Bauwettbewerb
197 description Build the best creation for the given theme before time runs out and win through the other players’ ratings. Baue in der Zeit das beste Bauwerk zum Thema und gewinne durch die Bewertung der anderen Spieler.
198 seconds Build Time Bauzeit
199
200 ns:game_MobArena#
201 name Mob Arena Mob-Arena
202 description Fight hordes of monsters with a sweeping sword and collect points for every kill. Dying costs points! Kämpfe mit deinem Schwert gegen Horden von Monstern und sammle Punkte für jeden Kill. Sterben kostet Punkte!
203 death You died! Du bist gestorben!
204 difficulty Difficulty Schwierigkeit
205 deathPenalty Points lost on death Punktabzug bei Tod
206 powerUps Power-ups Power-ups
207 powerUp_heal Healed! Geheilt!
208 powerUp_strength Double damage for 10 seconds! Doppelter Schaden für 10 Sekunden!
209 powerUp_speed Speed for 10 seconds! Schneller für 10 Sekunden!
210
211 ns:game_Backrooms#
212 name Backrooms Backrooms
213 description Escape the Backrooms before the others do - or be the last one alive! Entkomme den Backrooms vor allen anderen - oder überlebe als Letzter!