Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90395f71e9 | ||
|
|
b6c3f2c1e0 | ||
|
|
e8e46c8e46 | ||
|
|
d021f12ad0 | ||
|
|
2d92eb7f0b | ||
|
|
9e757378c1 | ||
|
|
dc32b053a3 | ||
|
|
f77d1290c7 | ||
|
|
d51ad387b4 | ||
|
|
63afbaaf85 | ||
|
|
1ef1d760ed | ||
|
|
70be6418e0 | ||
|
|
9d779a4879 | ||
|
|
331efa9ffd | ||
|
|
f23495ecdd | ||
|
|
89b81a3288 | ||
|
|
7424d6b929 | ||
|
|
a18a6b0dca | ||
|
|
8dc165952a | ||
|
|
3f1d2b86f2 | ||
|
|
4bd03c2603 | ||
|
|
75cd304bda | ||
|
|
53b13b6560 | ||
|
|
7e0c126a0d | ||
|
|
096b035503 | ||
|
|
29d347ec03 | ||
|
|
103bd23bf4 | ||
|
|
b175433701 | ||
|
|
460d737d3d | ||
|
|
7591ce29ff | ||
|
|
39bbf19d9d | ||
|
|
df239d8166 | ||
|
|
bf9c16ccc8 | ||
|
|
b5d6d7bac4 | ||
|
|
3b55f16b24 | ||
|
|
5ef254b75f | ||
|
|
5e5f36153e | ||
|
|
e77879c657 | ||
|
|
e3068be160 | ||
|
|
867bee1c5a | ||
|
|
893923cc56 | ||
|
|
38c944e6c1 |
No files matched your search
+4
-1
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
//for jar packaging see https://github.com/Protonull/BasicMinestomServer/blob/master/build.gradle.kts
|
//for jar packaging see https://github.com/Protonull/BasicMinestomServer/blob/master/build.gradle.kts
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -11,6 +10,7 @@ version '1.0-SNAPSHOT'
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url = 'https://mvn.everbuild.org/public' }
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -38,6 +38,9 @@ dependencies {
|
|||||||
|
|
||||||
//PvP
|
//PvP
|
||||||
implementation 'io.github.togar2:MinestomPvP:2026.05.30-26.1.1'
|
implementation 'io.github.togar2:MinestomPvP:2026.05.30-26.1.1'
|
||||||
|
|
||||||
|
//Block placement
|
||||||
|
implementation 'org.everbuild.blocksandstuff:blocksandstuff-blocks:1.10.2-SNAPSHOT'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.mhsl.minenet.minigames.handler.global;
|
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.Icon;
|
||||||
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
|
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
@@ -9,6 +10,9 @@ import net.minestom.server.event.player.PlayerChatEvent;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class ChatFormatHandler implements EventListener<PlayerChatEvent> {
|
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
|
@Override
|
||||||
public @NotNull Class<PlayerChatEvent> eventType() {
|
public @NotNull Class<PlayerChatEvent> eventType() {
|
||||||
return PlayerChatEvent.class;
|
return PlayerChatEvent.class;
|
||||||
@@ -16,6 +20,10 @@ public class ChatFormatHandler implements EventListener<PlayerChatEvent> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Result run(@NotNull PlayerChatEvent event) {
|
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;
|
NamedTextColor color = event.getPlayer().getPermissionLevel() > 0 ? NamedTextColor.AQUA : NamedTextColor.GRAY;
|
||||||
event.setFormattedMessage(
|
event.setFormattedMessage(
|
||||||
new ChatMessage(Icon.CHAT)
|
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.blockBreakRace.BlockBreakRaceFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.boatRace.BoatRaceFactory;
|
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.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.colorJump.ColorJumpFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube.DeathcubeFactory;
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube.DeathcubeFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.elytraRace.ElytraRaceFactory;
|
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.highGround.HighGroundFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.jumpDive.JumpDiveFactory;
|
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.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.pillars.PillarsFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.PlayerRaceFactory;
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.PlayerRaceFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake.SpaceSnakeFactory;
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake.SpaceSnakeFactory;
|
||||||
@@ -54,7 +56,9 @@ public enum GameList {
|
|||||||
PILLARS(new PillarsFactory(), GameType.PROTOTYPE),
|
PILLARS(new PillarsFactory(), GameType.PROTOTYPE),
|
||||||
BLOCKBATTLE(new BlockBattleFactory(), GameType.PVP),
|
BLOCKBATTLE(new BlockBattleFactory(), GameType.PVP),
|
||||||
COLORJUMP(new ColorJumpFactory(), GameType.JUMPNRUN),
|
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 GameFactory factory;
|
||||||
private final GameType type;
|
private final GameType type;
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ public class BlockBattle extends StatelessGame {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> {
|
this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> {
|
||||||
if(this.isBeforeBeginning) finalAttackEvent.setCancelled(true);
|
if(!this.isRunning) finalAttackEvent.setCancelled(true);
|
||||||
finalAttackEvent.setBaseDamage(0);
|
finalAttackEvent.setBaseDamage(0);
|
||||||
((Player) finalAttackEvent.getTarget()).setHealth(20);
|
((Player) finalAttackEvent.getTarget()).setHealth(20);
|
||||||
});
|
});
|
||||||
|
|||||||
+226
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+85
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+267
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -172,5 +172,9 @@ public class ColorJump extends StatelessGame {
|
|||||||
this.getScore().insertResult(player);
|
this.getScore().insertResult(player);
|
||||||
player.setGameMode(GameMode.SPECTATOR);
|
player.setGameMode(GameMode.SPECTATOR);
|
||||||
}
|
}
|
||||||
|
if(!this.isBeforeBeginning && !this.isRunning) {
|
||||||
|
player.setGameMode(GameMode.SPECTATOR);
|
||||||
|
player.teleport(this.getSpawn());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+13
-9
@@ -9,6 +9,7 @@ import io.github.togar2.pvp.events.FinalAttackEvent;
|
|||||||
import io.github.togar2.pvp.events.PrepareAttackEvent;
|
import io.github.togar2.pvp.events.PrepareAttackEvent;
|
||||||
import io.github.togar2.pvp.feature.CombatFeatures;
|
import io.github.togar2.pvp.feature.CombatFeatures;
|
||||||
import io.github.togar2.pvp.feature.knockback.KnockbackSettings;
|
import io.github.togar2.pvp.feature.knockback.KnockbackSettings;
|
||||||
|
import net.minestom.server.ServerFlag;
|
||||||
import net.minestom.server.coordinate.Pos;
|
import net.minestom.server.coordinate.Pos;
|
||||||
import net.minestom.server.entity.Player;
|
import net.minestom.server.entity.Player;
|
||||||
import net.minestom.server.event.instance.InstanceTickEvent;
|
import net.minestom.server.event.instance.InstanceTickEvent;
|
||||||
@@ -51,15 +52,18 @@ class HighGround extends StatelessGame {
|
|||||||
|
|
||||||
this.eventNode().addListener(
|
this.eventNode().addListener(
|
||||||
EntityKnockbackEvent.class,
|
EntityKnockbackEvent.class,
|
||||||
entityKnockbackEvent -> entityKnockbackEvent.setSettings(
|
entityKnockbackEvent -> {
|
||||||
KnockbackSettings.builder()
|
double tps = ServerFlag.SERVER_TICKS_PER_SECOND;
|
||||||
.horizontal(1.1)
|
entityKnockbackEvent.setSettings(
|
||||||
.vertical(KnockbackSettings.DEFAULT.vertical())
|
KnockbackSettings.builder()
|
||||||
.verticalLimit(KnockbackSettings.DEFAULT.verticalLimit())
|
.horizontal(1.1)
|
||||||
.extraHorizontal(KnockbackSettings.DEFAULT.extraHorizontal())
|
.vertical(KnockbackSettings.DEFAULT.vertical() / tps)
|
||||||
.extraVertical(KnockbackSettings.DEFAULT.extraVertical())
|
.verticalLimit(KnockbackSettings.DEFAULT.verticalLimit() / tps)
|
||||||
.build()
|
.extraHorizontal(KnockbackSettings.DEFAULT.extraHorizontal() / tps)
|
||||||
)
|
.extraVertical(KnockbackSettings.DEFAULT.extraVertical() / tps)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.eventNode().addListener(InstanceTickEvent.class, instanceTickEvent -> {
|
this.eventNode().addListener(InstanceTickEvent.class, instanceTickEvent -> {
|
||||||
|
|||||||
+18
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+342
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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)];
|
||||||
|
}
|
||||||
|
}
|
||||||
+193
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-2
@@ -147,10 +147,10 @@ class Tetris extends StatelessGame {
|
|||||||
|
|
||||||
TetrisGame tetrisGame = this.tetrisGames.get(p);
|
TetrisGame tetrisGame = this.tetrisGames.get(p);
|
||||||
|
|
||||||
p.teleport(tetrisGame.getPlayerSpawnPosition());
|
|
||||||
tetrisGame.sidebar.addViewer(p);
|
tetrisGame.sidebar.addViewer(p);
|
||||||
|
|
||||||
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
|
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
|
||||||
|
p.teleport(tetrisGame.getPlayerSpawnPosition());
|
||||||
Entity ghostBlock = new Entity(EntityType.BLOCK_DISPLAY);
|
Entity ghostBlock = new Entity(EntityType.BLOCK_DISPLAY);
|
||||||
((BlockDisplayMeta) ghostBlock.getEntityMeta()).setBlockState(Block.AIR);
|
((BlockDisplayMeta) ghostBlock.getEntityMeta()).setBlockState(Block.AIR);
|
||||||
ghostBlock.setNoGravity(true);
|
ghostBlock.setNoGravity(true);
|
||||||
@@ -159,7 +159,6 @@ class Tetris extends StatelessGame {
|
|||||||
return TaskSchedule.stop();
|
return TaskSchedule.stop();
|
||||||
}, TaskSchedule.nextTick());
|
}, TaskSchedule.nextTick());
|
||||||
|
|
||||||
|
|
||||||
return super.onPlayerJoin(p);
|
return super.onPlayerJoin(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@ public class TetrisGame {
|
|||||||
int standardTickDelay = 40;
|
int standardTickDelay = 40;
|
||||||
if(this.isFast) standardTickDelay = 20;
|
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;
|
if(this.paused) return nextTick;
|
||||||
this.tick();
|
this.tick();
|
||||||
return nextTick;
|
return nextTick;
|
||||||
|
|||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.Tower;
|
||||||
|
import net.minestom.server.coordinate.Point;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.coordinate.Vec;
|
||||||
|
import net.minestom.server.entity.Entity;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.entity.Player;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
import net.minestom.server.network.packet.server.SendablePacket;
|
||||||
|
import net.minestom.server.network.packet.server.play.ParticlePacket;
|
||||||
|
import net.minestom.server.particle.Particle;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class Cursor extends Entity {
|
||||||
|
private boolean cursorActive = false;
|
||||||
|
private final int reach;
|
||||||
|
|
||||||
|
public Cursor(int reach) {
|
||||||
|
super(EntityType.ARMOR_STAND);
|
||||||
|
this.setBoundingBox(0,0,0);
|
||||||
|
this.reach = reach;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCursorPosition(Player player) {
|
||||||
|
Point targetPosition = player.getTargetBlockPosition(this.reach);
|
||||||
|
this.moveCursorTo(targetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveCursorTo(@Nullable Point newTargetBlockPosition) {
|
||||||
|
this.setCursorEnabled(true);
|
||||||
|
if(newTargetBlockPosition == null) {
|
||||||
|
this.setCursorEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!this.getInstance().getBlock(newTargetBlockPosition).equals(Block.BLACK_WOOL)) this.setCursorEnabled(false);
|
||||||
|
Material holdingMaterial = this.getInstance().getPlayerHandMaterial();
|
||||||
|
if(!this.getInstance().getGame().getAvailableTowers().containsKey(holdingMaterial)) this.setCursorEnabled(false);
|
||||||
|
this.teleport(new Pos(newTargetBlockPosition.add(0.5,1,0.5)));
|
||||||
|
this.showRange(this.getInstance().getGame().getAvailableTowers().get(holdingMaterial));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCursorEnabled(boolean enabled) {
|
||||||
|
this.cursorActive = enabled;
|
||||||
|
this.setInvisible(!enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showRange(@Nullable Class<? extends Tower> towerClass) {
|
||||||
|
if(towerClass == null) return;
|
||||||
|
if(!this.isCursorActive()) return;
|
||||||
|
try {
|
||||||
|
int range = towerClass.getConstructor().newInstance().getRange();
|
||||||
|
Collection<SendablePacket> particles = new ArrayList<>();
|
||||||
|
double circumference = 2 * Math.PI * range;
|
||||||
|
int count = (int) (circumference * 1.5);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
double radians = ((2 * Math.PI) / count) * i;
|
||||||
|
Vec relativePosition = new Vec(Math.sin(radians)*range, 0, Math.cos(radians)*range);
|
||||||
|
ParticlePacket particle = new ParticlePacket(Particle.COMPOSTER, this.position.add(relativePosition), Pos.ZERO, 0, 1);
|
||||||
|
particles.add(particle);
|
||||||
|
}
|
||||||
|
this.getInstance().getPlayer().sendPackets(particles);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TowerdefenseRoom getInstance() {
|
||||||
|
return (TowerdefenseRoom) super.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCursorActive() {
|
||||||
|
return this.cursorActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Block getTargetBlock() {
|
||||||
|
return this.getInstance().getBlock(this.getTargetBlockPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pos getTargetBlockPosition() {
|
||||||
|
return this.getPosition().sub(0, 0.5, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+145
-13
@@ -2,40 +2,159 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
|
|||||||
|
|
||||||
import eu.mhsl.minenet.minigames.instance.Dimension;
|
import eu.mhsl.minenet.minigames.instance.Dimension;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
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.generator.MazeGenerator;
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.*;
|
||||||
import eu.mhsl.minenet.minigames.score.LastWinsScore;
|
import eu.mhsl.minenet.minigames.score.LastWinsScore;
|
||||||
|
import net.kyori.adventure.sound.Sound;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.title.Title;
|
||||||
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.coordinate.Pos;
|
import net.minestom.server.coordinate.Pos;
|
||||||
import net.minestom.server.entity.EntityType;
|
|
||||||
import net.minestom.server.entity.Player;
|
import net.minestom.server.entity.Player;
|
||||||
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
|
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.item.ItemStack;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
import net.minestom.server.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.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class Towerdefense extends StatelessGame {
|
public class Towerdefense extends StatelessGame {
|
||||||
private final Random random = new Random();
|
private final Random random = new Random();
|
||||||
private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch();
|
private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch();
|
||||||
private final List<Pos> mazePath = new ArrayList<>();
|
private final List<Pos> mazePath = new ArrayList<>();
|
||||||
private final List<TowerdefenseRoom> instances = new ArrayList<>();
|
private final List<TowerdefenseRoom> instances = new ArrayList<>();
|
||||||
|
private final Set<Player> spectators = new HashSet<>();
|
||||||
|
private final WaveGenerator waveGenerator;
|
||||||
|
private 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() {
|
public Towerdefense() {
|
||||||
super(Dimension.NETHER.key, "Towerdefense", new LastWinsScore());
|
super(Dimension.NETHER.key, "Towerdefense", new LastWinsScore());
|
||||||
|
|
||||||
this.setGenerator(new MazeGenerator());
|
this.setGenerator(new MazeGenerator());
|
||||||
this.generateMaze();
|
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() {
|
private void generateMaze() {
|
||||||
Pos position = new Pos(0, 0, 0);
|
Pos position = new Pos(0, 0, 0);
|
||||||
this.addMazePosition(position, Block.GREEN_WOOL);
|
this.addMazePosition(position, Block.GREEN_WOOL);
|
||||||
|
position = position.add(0,0,2);
|
||||||
|
|
||||||
List<Integer> previousDirections = new ArrayList<>();
|
List<Integer> previousDirections = new ArrayList<>();
|
||||||
int direction = 1; // 0 -> right; 1 -> straight; 2 -> left
|
int direction = 1; // 0 -> right; 1 -> straight; 2 -> left
|
||||||
for(int i = 0; i < 9; i++) {
|
for (int i = 0; i < pathLength; i++) {
|
||||||
for(int j = 0; j < 3; j++) {
|
for (int j = 0; j < 9; j++) {
|
||||||
position = position.add(direction - 1, 0, direction % 2);
|
position = position.add(direction-1,0,direction%2);
|
||||||
this.addMazePosition(position, Block.WHITE_WOOL);
|
this.addMazePosition(position, Block.WHITE_WOOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +167,17 @@ public class Towerdefense extends StatelessGame {
|
|||||||
previousDirections.add(direction);
|
previousDirections.add(direction);
|
||||||
}
|
}
|
||||||
this.addMazePosition(position, Block.WHITE_WOOL);
|
this.addMazePosition(position, Block.WHITE_WOOL);
|
||||||
this.addMazePosition(position.add(0, 0, 1), Block.WHITE_WOOL);
|
this.addMazePosition(position.add(0,0,3), Block.WHITE_WOOL);
|
||||||
this.addMazePosition(position.add(0, 0, 2), Block.RED_WOOL);
|
this.addMazePosition(position.add(0,0,6), Block.RED_WOOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addMazePosition(Pos position, Block pathBlock) {
|
private void addMazePosition(Pos position, Block pathBlock) {
|
||||||
this.mazeBatch.setBlock(position, pathBlock);
|
for (int i = 0; i < 3; i++) {
|
||||||
this.mazePath.add(position.add(0.5, 1, 0.5));
|
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() {
|
public AbsoluteBlockBatch getMazeBatch() {
|
||||||
@@ -65,12 +188,21 @@ public class Towerdefense extends StatelessGame {
|
|||||||
return this.mazePath;
|
return this.mazePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<TowerdefenseRoom> getInstances() {
|
||||||
|
return this.instances;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onPlayerJoin(Player p) {
|
protected boolean onPlayerJoin(Player p) {
|
||||||
TowerdefenseRoom newRoom = new TowerdefenseRoom(p, this);
|
MinecraftServer.getSchedulerManager().scheduleNextTick(() -> p.teleport(new Pos((long) -this.getPlayers().size(), 1, 0)));
|
||||||
this.instances.add(newRoom);
|
return super.onPlayerJoin(p);
|
||||||
p.setInstance(newRoom);
|
}
|
||||||
newRoom.startWave(List.of(EntityType.ENDERMAN, EntityType.BLAZE, EntityType.PLAYER, EntityType.HORSE, EntityType.ARMOR_STAND, EntityType.SKELETON));
|
|
||||||
return false;
|
public Map<Class<? extends Tower>, Integer> getPrices() {
|
||||||
|
return this.prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Material, Class<? extends Tower>> getAvailableTowers() {
|
||||||
|
return this.availableTowers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+252
-29
@@ -1,67 +1,290 @@
|
|||||||
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
|
||||||
|
|
||||||
import eu.mhsl.minenet.minigames.instance.Dimension;
|
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.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.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.MinecraftServer;
|
||||||
import net.minestom.server.entity.EntityCreature;
|
import net.minestom.server.collision.Aerodynamics;
|
||||||
import net.minestom.server.entity.EntityType;
|
import net.minestom.server.coordinate.Pos;
|
||||||
import net.minestom.server.entity.GameMode;
|
import net.minestom.server.coordinate.Vec;
|
||||||
import net.minestom.server.entity.Player;
|
import net.minestom.server.entity.*;
|
||||||
import net.minestom.server.entity.attribute.Attribute;
|
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.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.ItemStack;
|
||||||
import net.minestom.server.item.Material;
|
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.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class TowerdefenseRoom extends InstanceContainer {
|
public class TowerdefenseRoom extends InstanceContainer {
|
||||||
|
private final static int reach = 30;
|
||||||
|
private long lastPlayerAttack = 0;
|
||||||
private final Player player;
|
private final Player player;
|
||||||
private final Towerdefense game;
|
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) {
|
public TowerdefenseRoom(Player player, Towerdefense game) {
|
||||||
this(UUID.randomUUID(), player, game);
|
super(UUID.randomUUID(), Dimension.OVERWORLD.key);
|
||||||
}
|
|
||||||
|
|
||||||
// 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));
|
|
||||||
MinecraftServer.getInstanceManager().registerInstance(this);
|
MinecraftServer.getInstanceManager().registerInstance(this);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.game = game;
|
this.game = game;
|
||||||
this.player.setGameMode(GameMode.ADVENTURE);
|
this.player.setGameMode(GameMode.ADVENTURE);
|
||||||
this.player.setAllowFlying(true);
|
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());
|
this.setGenerator(new MazeGenerator());
|
||||||
BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {
|
BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {});
|
||||||
|
|
||||||
|
this.cursor = new Cursor(reach);
|
||||||
|
this.cursor.setInstance(this);
|
||||||
|
this.cursor.setBoundingBox(0,0,0);
|
||||||
|
|
||||||
|
this.eventNode()
|
||||||
|
.addListener(EntityDeathEvent.class, event -> {
|
||||||
|
if(!(event.getEntity() instanceof EntityCreature enemy)) return;
|
||||||
|
this.enemies.remove(enemy);
|
||||||
|
})
|
||||||
|
.addListener(PlayerTickEvent.class, event -> {
|
||||||
|
if(event.getPlayer() != this.player || this.dead) return;
|
||||||
|
this.cursor.updateCursorPosition(this.player);
|
||||||
|
})
|
||||||
|
.addListener(PlayerUseItemEvent.class, event -> this.handleUse(event.getPlayer()))
|
||||||
|
.addListener(PlayerUseItemOnBlockEvent.class, event -> this.handleUse(event.getPlayer()))
|
||||||
|
.addListener(PlayerEntityInteractEvent.class, event -> this.handleUse(event.getPlayer()))
|
||||||
|
.addListener(PlayerHandAnimationEvent.class, event -> this.handleUse(event.getPlayer()))
|
||||||
|
.addListener(ItemDropEvent.class, CommonEventHandles::cancel)
|
||||||
|
.addListener(InventoryPreClickEvent.class, CommonEventHandles::cancel)
|
||||||
|
.addListener(PlayerSwapItemEvent.class, CommonEventHandles::cancel)
|
||||||
|
.addListener(PlayerBeginItemUseEvent.class, event -> {
|
||||||
|
if(event.getAnimation().equals(ItemAnimation.BOW)) event.setCancelled(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUse(Player eventPlayer) {
|
||||||
|
if(this.game.getSpectators().contains(eventPlayer)) {
|
||||||
|
if(eventPlayer.getItemInMainHand().material() == Material.COMPASS) {
|
||||||
|
this.game.cycleSpectator(eventPlayer);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(eventPlayer != this.player || this.dead) return;
|
||||||
|
this.useItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void useItem() {
|
||||||
|
Material itemInHand = this.player.getItemInMainHand().material();
|
||||||
|
if(itemInHand.equals(Material.BOW)) {
|
||||||
|
this.playerAttack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(itemInHand.equals(Material.BARRIER)) {
|
||||||
|
this.removeTower();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.placeTower();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playerAttack() {
|
||||||
|
if(this.lastPlayerAttack > System.currentTimeMillis()-200) return;
|
||||||
|
Player p = this.player;
|
||||||
|
this.lastPlayerAttack = System.currentTimeMillis();
|
||||||
|
EntityProjectile projectile = new EntityProjectile(p, EntityType.SPECTRAL_ARROW);
|
||||||
|
projectile.setNoGravity(true);
|
||||||
|
projectile.setAerodynamics(new Aerodynamics(0, 0.99, 0.99));
|
||||||
|
projectile.setView(p.getPosition().yaw(), p.getPosition().pitch());
|
||||||
|
|
||||||
|
Pos spawn = p.getPosition().add(0, p.getEyeHeight() - 0.2, 0);
|
||||||
|
projectile.scheduleRemove(Duration.ofSeconds(5));
|
||||||
|
projectile.eventNode()
|
||||||
|
.addListener(ProjectileCollideWithEntityEvent.class, hitEvent -> {
|
||||||
|
if(hitEvent.getTarget() == projectile.getShooter()) return;
|
||||||
|
try {
|
||||||
|
if(hitEvent.getTarget() instanceof EntityCreature target
|
||||||
|
&& !target.isDead()
|
||||||
|
&& this.enemies.contains(target)) {
|
||||||
|
target.damage(DamageType.PLAYER_ATTACK, 1.2f);
|
||||||
|
if(target.isDead()) this.addMoney(Math.max(1, (int) target.getAttribute(Attribute.MAX_HEALTH).getBaseValue() / 3));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
projectile.remove();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addListener(ProjectileCollideWithBlockEvent.class, collisionEvent -> projectile.remove());
|
||||||
|
projectile.setInstance(this, spawn).thenRun(() -> {
|
||||||
|
Vec projectileVelocity = p.getPosition().direction().normalize().mul(60);
|
||||||
|
projectile.setVelocity(projectileVelocity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startWave(List<EntityType> entities) {
|
public void addMoney(int amount) {
|
||||||
int counter = 0;
|
this.money += amount;
|
||||||
for(EntityType entityType : entities) {
|
this.player.setLevel(this.money);
|
||||||
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
|
|
||||||
this.addEntity(new EntityCreature(entityType));
|
|
||||||
return TaskSchedule.stop();
|
|
||||||
}, TaskSchedule.millis(800L * counter));
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addEntity(EntityCreature entity) {
|
public Material getPlayerHandMaterial() {
|
||||||
entity.setInstance(this, this.game.getMazePath().getFirst());
|
return this.player.getItemInMainHand().material();
|
||||||
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) {
|
private synchronized void placeTower() {
|
||||||
if(positionIndex == this.game.getMazePath().size() - 1) {
|
if(!this.canPlaceActiveTower()) {
|
||||||
|
if(this.cursor.isCursorActive() && !this.enoughMoneyForActiveTower()) {
|
||||||
|
this.player.sendActionBar(Component.text("Nicht genug Geld!", TextColor.color(255,0,0)));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entity.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex + 1), 0.7, () -> this.changeEntityGoal(entity, positionIndex + 1));
|
try {
|
||||||
|
Material usedMaterial = this.player.getItemInMainHand().material();
|
||||||
|
Tower tower = this.game.getAvailableTowers().entrySet().stream()
|
||||||
|
.filter(materialClassEntry -> materialClassEntry.getKey().equals(usedMaterial))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow()
|
||||||
|
.getValue()
|
||||||
|
.getConstructor()
|
||||||
|
.newInstance();
|
||||||
|
this.setBlock(this.cursor.getTargetBlockPosition(), Block.BLUE_WOOL);
|
||||||
|
tower.setInstance(this, this.cursor.getPosition());
|
||||||
|
this.towers.add(tower);
|
||||||
|
tower.startShooting();
|
||||||
|
this.addMoney(-this.game.getPrices().get(tower.getClass()));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
this.cursor.updateCursorPosition(this.player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void removeTower() {
|
||||||
|
Tower tower = null;
|
||||||
|
Entity entity = this.player.getLineOfSightEntity(reach, e -> e instanceof Tower);
|
||||||
|
if(entity instanceof Tower t && this.towers.contains(t)) {
|
||||||
|
tower = t;
|
||||||
|
} else if(this.cursor.getTargetBlockPosition() != null) {
|
||||||
|
Pos target = new Pos(this.cursor.getTargetBlockPosition()).add(0.5, 0.5, 0.5);
|
||||||
|
tower = this.towers.stream()
|
||||||
|
.filter(t -> t.getPosition().distanceSquared(target) <= 4)
|
||||||
|
.min(java.util.Comparator.comparingDouble(t -> t.getPosition().distanceSquared(target)))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
if(tower == null) return;
|
||||||
|
|
||||||
|
this.setBlock(tower.getPosition().sub(0, 0.5, 0), Block.BLACK_WOOL);
|
||||||
|
this.addMoney(tower.getSellingPrice(this.game.getPrices().get(tower.getClass())));
|
||||||
|
this.towers.remove(tower);
|
||||||
|
tower.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canPlaceActiveTower() {
|
||||||
|
Material usedMaterial = this.player.getItemInMainHand().material();
|
||||||
|
return this.canPlaceTower(this.game.getAvailableTowers().get(usedMaterial));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canPlaceTower(@Nullable Class<? extends Tower> towerClass) {
|
||||||
|
if(towerClass == null) return false;
|
||||||
|
return this.cursor.isCursorActive() && this.enoughMoney(towerClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean enoughMoneyForActiveTower() {
|
||||||
|
Material usedMaterial = this.player.getItemInMainHand().material();
|
||||||
|
return this.enoughMoney(this.game.getAvailableTowers().get(usedMaterial));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean enoughMoney(@Nullable Class<? extends Tower> towerClass) {
|
||||||
|
if(towerClass == null) return true;
|
||||||
|
return this.money >= this.game.getPrices().get(towerClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startWave(List<GroupFactory> enemyGroups) {
|
||||||
|
enemyGroups.forEach(groupFactory -> groupFactory.summonGroup(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addEnemy(EntityCreature enemy) {
|
||||||
|
enemy.setInstance(this, this.game.getMazePath().getFirst());
|
||||||
|
this.enemies.add(enemy);
|
||||||
|
this.changeEnemyGoal(enemy, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void changeEnemyGoal(EntityCreature enemy, int positionIndex) {
|
||||||
|
if(positionIndex == this.game.getMazePath().size()-1) {
|
||||||
|
this.enemies.remove(enemy);
|
||||||
|
enemy.remove();
|
||||||
|
if(this.dead) return;
|
||||||
|
float damage = (float) Math.ceil(enemy.getHealth()/10);
|
||||||
|
if(this.player.getHealth() - damage <= 0) {
|
||||||
|
this.handleDeath();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.player.damage(DamageType.PLAYER_ATTACK, damage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enemy.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.6, () -> this.changeEnemyGoal(enemy, positionIndex+1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDeath() {
|
||||||
|
this.dead = true;
|
||||||
|
this.game.removeInstance(this);
|
||||||
|
new ArrayList<>(this.enemies).forEach(Entity::remove);
|
||||||
|
this.enemies.clear();
|
||||||
|
new ArrayList<>(this.towers).forEach(Entity::remove);
|
||||||
|
this.towers.clear();
|
||||||
|
this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).attribute().defaultValue());
|
||||||
|
this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).attribute().defaultValue());
|
||||||
|
this.player.heal();
|
||||||
|
this.game.getScore().insertResult(this.player);
|
||||||
|
if(!this.game.getAliveInstances().isEmpty()) {
|
||||||
|
this.game.beginSpectating(this.player);
|
||||||
|
} else {
|
||||||
|
this.player.setInstance(this.game).thenRun(() -> MinecraftServer.getInstanceManager().unregisterInstance(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDead() {
|
||||||
|
return this.dead;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Towerdefense getGame() {
|
||||||
|
return this.game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EntityCreature> getEnemies() {
|
||||||
|
return this.enemies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getPlayer() {
|
||||||
|
return this.player;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.entity.attribute.Attribute;
|
||||||
|
|
||||||
|
public record EnemyFactory(EntityType entityType, float health, double speed) {
|
||||||
|
/**
|
||||||
|
* Factory for a tower defense enemy.
|
||||||
|
* @param entityType type of enemy
|
||||||
|
* @param health base health (between 0 and 1024, default 10)
|
||||||
|
* @param speed walk speed (default 0.1)
|
||||||
|
*/
|
||||||
|
public EnemyFactory {
|
||||||
|
if(health > 1024 || health <= 0) throw new IllegalArgumentException("Enemy health has to be between 0 and 1024");
|
||||||
|
}
|
||||||
|
|
||||||
|
public EnemyFactory(EntityType entityType) {
|
||||||
|
this(entityType, 10, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityCreature buildEntity() {
|
||||||
|
EntityCreature entity = new EntityCreature(this.entityType);
|
||||||
|
entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(this.speed);
|
||||||
|
entity.getAttribute(Attribute.MAX_HEALTH).setBaseValue(this.health);
|
||||||
|
entity.setHealth(this.health);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStrength() {
|
||||||
|
return (int) Math.floor(this.health * this.speed * 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseRoom;
|
||||||
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
|
||||||
|
public record GroupFactory(EnemyFactory enemyFactory, int count, long delay) {
|
||||||
|
public void summonGroup(TowerdefenseRoom instance) {
|
||||||
|
for (int i = 0; i < this.count; i++) {
|
||||||
|
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
|
||||||
|
instance.addEnemy(this.enemyFactory.buildEntity());
|
||||||
|
return TaskSchedule.stop();
|
||||||
|
}, TaskSchedule.millis(this.delay*i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.Towerdefense;
|
||||||
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public class WaveGenerator {
|
||||||
|
final Towerdefense game;
|
||||||
|
int currentLevel;
|
||||||
|
Random random = new Random();
|
||||||
|
private final Map<EnemyFactory, Integer> availableEnemies = Map.of(
|
||||||
|
new EnemyFactory(EntityType.PIG, 2, 0.1), new EnemyFactory(EntityType.PIG, 2, 0.1).getStrength(),
|
||||||
|
new EnemyFactory(EntityType.VILLAGER, 6, 0.1), new EnemyFactory(EntityType.VILLAGER, 6, 0.1).getStrength(),
|
||||||
|
new EnemyFactory(EntityType.SHEEP, 4, 0.2), new EnemyFactory(EntityType.SHEEP, 4, 0.2).getStrength(),
|
||||||
|
new EnemyFactory(EntityType.COW, 10, 0.1), new EnemyFactory(EntityType.COW, 10, 0.1).getStrength(),
|
||||||
|
new EnemyFactory(EntityType.CAMEL, 10, 0.2), new EnemyFactory(EntityType.CAMEL, 10, 0.2).getStrength(),
|
||||||
|
new EnemyFactory(EntityType.ARMADILLO, 20, 0.4), new EnemyFactory(EntityType.ARMADILLO, 20, 0.3).getStrength(),
|
||||||
|
new EnemyFactory(EntityType.CHICKEN, 8, 0.5), new EnemyFactory(EntityType.CHICKEN, 8, 0.6).getStrength()
|
||||||
|
);
|
||||||
|
|
||||||
|
public WaveGenerator(Towerdefense game) {
|
||||||
|
this(game, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaveGenerator(Towerdefense game, int startLevel) {
|
||||||
|
this.game = game;
|
||||||
|
this.currentLevel = startLevel - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int MAX_ENEMIES_PER_WAVE = 60;
|
||||||
|
private static final int MAX_ALIVE_PER_ROOM = 150;
|
||||||
|
|
||||||
|
public void startNextWave() {
|
||||||
|
this.currentLevel += 1;
|
||||||
|
if(this.game.getAliveInstances().isEmpty()) return;
|
||||||
|
List<EnemyFactory> enemyList = this.chooseEnemies();
|
||||||
|
if(enemyList.isEmpty()) return;
|
||||||
|
Collections.shuffle(enemyList);
|
||||||
|
|
||||||
|
int averageDelay = Math.min(20000 / enemyList.size(), 1500);
|
||||||
|
AtomicInteger delay = new AtomicInteger(0);
|
||||||
|
for (EnemyFactory enemy : enemyList) {
|
||||||
|
delay.addAndGet(averageDelay);
|
||||||
|
MinecraftServer.getSchedulerManager().scheduleTask(
|
||||||
|
() -> this.spawnEnemy(enemy),
|
||||||
|
TaskSchedule.millis(delay.get() + this.random.nextInt(averageDelay-200, averageDelay+200)),
|
||||||
|
TaskSchedule.stop()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnEnemy(EnemyFactory enemy) {
|
||||||
|
float hpMultiplier = this.getHealthMultiplier();
|
||||||
|
this.game.getAliveInstances().forEach(instance -> {
|
||||||
|
if(instance.isDead()) return;
|
||||||
|
if(instance.getEnemies().size() >= MAX_ALIVE_PER_ROOM) return;
|
||||||
|
try {
|
||||||
|
EntityCreature entity = enemy.buildEntity();
|
||||||
|
if(hpMultiplier > 1f) {
|
||||||
|
float scaledHp = (float) Math.min(1024, entity.getAttribute(net.minestom.server.entity.attribute.Attribute.MAX_HEALTH).getBaseValue() * hpMultiplier);
|
||||||
|
entity.getAttribute(net.minestom.server.entity.attribute.Attribute.MAX_HEALTH).setBaseValue(scaledHp);
|
||||||
|
entity.setHealth(scaledHp);
|
||||||
|
}
|
||||||
|
instance.addEnemy(entity);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private float getHealthMultiplier() {
|
||||||
|
// Levels 1-3: unchanged. Then +12% HP per level, capped so tower
|
||||||
|
// damage still matters at very high levels.
|
||||||
|
return Math.min(5f, 1f + Math.max(0, this.currentLevel - 3) * 0.12f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getCurrentLevelStrength() {
|
||||||
|
// Linear scaling (was 0.5 * 2^level — exponential blow-up).
|
||||||
|
int alive = Math.max(1, this.game.getAliveInstances().size());
|
||||||
|
return (int) (15 + this.currentLevel * 6 * Math.sqrt(alive));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EnemyFactory> chooseEnemies() {
|
||||||
|
int strength = this.getCurrentLevelStrength();
|
||||||
|
final List<EnemyFactory> usedEnemies = new ArrayList<>();
|
||||||
|
|
||||||
|
while(strength > 0 && usedEnemies.size() < MAX_ENEMIES_PER_WAVE) {
|
||||||
|
int finalStrength = strength;
|
||||||
|
Optional<Map.Entry<EnemyFactory, Integer>> chosenEnemy = this.availableEnemies.entrySet().stream()
|
||||||
|
.filter(e -> e.getValue() <= finalStrength)
|
||||||
|
.sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2))
|
||||||
|
.findAny();
|
||||||
|
if(chosenEnemy.isEmpty()) break;
|
||||||
|
usedEnemies.add(chosenEnemy.get().getKey());
|
||||||
|
strength -= chosenEnemy.get().getKey().getStrength();
|
||||||
|
}
|
||||||
|
|
||||||
|
return usedEnemies;
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
|
||||||
|
public class BlazeTower extends ShootingTower {
|
||||||
|
public BlazeTower() {
|
||||||
|
super(EntityType.BLAZE, 2, 2, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.swingMainHand();
|
||||||
|
this.shootProjectile(enemy, EntityType.FIREBALL, 2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.item.ItemStack;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
|
||||||
|
public class BruteTower extends Tower {
|
||||||
|
public BruteTower() {
|
||||||
|
super(EntityType.PIGLIN_BRUTE, 20, 2, 4);
|
||||||
|
this.setItemInMainHand(ItemStack.of(Material.GOLDEN_AXE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.swingMainHand();
|
||||||
|
this.causeDamage(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
|
||||||
|
public class EnderDragonTower extends ShootingTower{
|
||||||
|
public EnderDragonTower() {
|
||||||
|
super(EntityType.ENDER_DRAGON, 20, 3, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.shootProjectile(enemy, EntityType.DRAGON_FIREBALL, 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProjectileHit(EntityCreature enemy) {
|
||||||
|
super.onProjectileHit(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
|
||||||
|
public class EndermanTower extends Tower {
|
||||||
|
public EndermanTower() {
|
||||||
|
super(EntityType.ENDERMAN, 12, 1, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.swingMainHand();
|
||||||
|
this.causeDamage(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.collision.Aerodynamics;
|
||||||
|
import net.minestom.server.coordinate.Point;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.coordinate.Vec;
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityProjectile;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.entity.attribute.Attribute;
|
||||||
|
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
|
||||||
|
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public abstract class ShootingTower extends Tower {
|
||||||
|
public ShootingTower(@NotNull EntityType entityType, int damage, double attacksPerSecond, int range) {
|
||||||
|
super(entityType, damage, attacksPerSecond, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void shootProjectile(EntityCreature enemy, EntityType projectileType, double power, double spread) {
|
||||||
|
EntityProjectile projectile = new EntityProjectile(this, projectileType);
|
||||||
|
projectile.setView(this.getPosition().yaw(), this.getPosition().pitch());
|
||||||
|
projectile.setNoGravity(true);
|
||||||
|
projectile.setAerodynamics(new Aerodynamics(0, 1, 1));
|
||||||
|
Pos startingPoint = this.getPosition().add(0, this.getEyeHeight(), 0);
|
||||||
|
|
||||||
|
double enemySpeed = enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue() / 0.05;
|
||||||
|
Point enemyGoal = enemy.getNavigator().getGoalPosition();
|
||||||
|
if(enemyGoal == null) enemyGoal = enemy.getPosition();
|
||||||
|
Pos enemyPosition = enemy.getPosition();
|
||||||
|
Vec enemyMovement = Vec.fromPoint(enemyGoal.sub(enemyPosition)).normalize().mul(enemySpeed);
|
||||||
|
|
||||||
|
// just an approximation, not calculated correctly:
|
||||||
|
double projectileSpeed = 20 * power;
|
||||||
|
double enemyTowerDistance = startingPoint.distance(enemyPosition);
|
||||||
|
double estimatedFlightTime = (enemyTowerDistance / projectileSpeed);
|
||||||
|
Pos targetPosition = enemyPosition.add(enemyMovement.mul(estimatedFlightTime)).withY(enemyPosition.y()+enemy.getEyeHeight());
|
||||||
|
|
||||||
|
projectile.shoot(targetPosition, power, spread);
|
||||||
|
projectile.scheduleRemove(Duration.ofSeconds(5));
|
||||||
|
projectile.setInstance(this.getInstance(), startingPoint);
|
||||||
|
projectile.eventNode()
|
||||||
|
.addListener(ProjectileCollideWithEntityEvent.class, event -> {
|
||||||
|
if(event.getTarget() == projectile.getShooter()) return;
|
||||||
|
try {
|
||||||
|
if(event.getTarget() instanceof EntityCreature target
|
||||||
|
&& !target.isDead()
|
||||||
|
&& this.getRoomInstance().getEnemies().contains(target)) {
|
||||||
|
this.causeDamage(target);
|
||||||
|
this.onProjectileHit(target);
|
||||||
|
if(target.isDead()) target.setFlyingWithElytra(true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
projectile.remove();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onProjectileHit(EntityCreature enemy) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.item.ItemStack;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
|
||||||
|
public class SkeletonTower extends ShootingTower {
|
||||||
|
public SkeletonTower() {
|
||||||
|
super(EntityType.SKELETON, 3, 2.5, 12);
|
||||||
|
this.setItemInMainHand(ItemStack.of(Material.BOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.swingMainHand();
|
||||||
|
this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProjectileHit(EntityCreature enemy) {
|
||||||
|
super.onProjectileHit(enemy);
|
||||||
|
// enemy.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue()*0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseRoom;
|
||||||
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.entity.*;
|
||||||
|
import net.minestom.server.entity.attribute.Attribute;
|
||||||
|
import net.minestom.server.entity.damage.DamageType;
|
||||||
|
import net.minestom.server.timer.Task;
|
||||||
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public abstract class Tower extends EntityCreature {
|
||||||
|
public enum Priority {
|
||||||
|
FIRST(Type.PATH_DISTANCE),
|
||||||
|
LAST(Type.PATH_DISTANCE),
|
||||||
|
STRONG(Type.HEALTH),
|
||||||
|
WEAK(Type.HEALTH),
|
||||||
|
CLOSE(Type.TOWER_DISTANCE),
|
||||||
|
FAR(Type.TOWER_DISTANCE);
|
||||||
|
|
||||||
|
final Type type;
|
||||||
|
Priority(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
PATH_DISTANCE,
|
||||||
|
HEALTH,
|
||||||
|
TOWER_DISTANCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static int damageDivider = 10;
|
||||||
|
private Priority priority = Priority.FIRST;
|
||||||
|
protected float damage;
|
||||||
|
protected int range;
|
||||||
|
protected double attacksPerSecond;
|
||||||
|
protected TaskSchedule attackDelay;
|
||||||
|
private @Nullable Task attackTask;
|
||||||
|
private float sellingPriceMultiplier = 1;
|
||||||
|
|
||||||
|
public Tower(@NotNull EntityType entityType, int damage, double attacksPerSecond, int range) {
|
||||||
|
super(entityType);
|
||||||
|
this.damage = (float) damage / damageDivider;
|
||||||
|
this.range = range;
|
||||||
|
this.attacksPerSecond = attacksPerSecond;
|
||||||
|
this.attackDelay = TaskSchedule.millis((long) (1000/attacksPerSecond));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startShooting() {
|
||||||
|
this.attackTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> {
|
||||||
|
EntityCreature nextEnemy = this.getNextEnemy();
|
||||||
|
if(nextEnemy == null) return this.attackDelay;
|
||||||
|
this.lookAt(nextEnemy);
|
||||||
|
this.attack(nextEnemy);
|
||||||
|
return this.attackDelay;
|
||||||
|
}, TaskSchedule.immediate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void remove(boolean permanent) {
|
||||||
|
if(this.attackTask != null) this.attackTask.cancel();
|
||||||
|
super.remove(permanent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable EntityCreature getNextEnemy() {
|
||||||
|
List<EntityCreature> enemies = this.getSortedEnemies();
|
||||||
|
if(enemies.isEmpty()) return null;
|
||||||
|
|
||||||
|
return switch (this.priority) {
|
||||||
|
case LAST, STRONG, FAR -> enemies.getLast();
|
||||||
|
case FIRST, WEAK, CLOSE -> enemies.getFirst();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EntityCreature> getSortedEnemies() {
|
||||||
|
Stream<EntityCreature> enemyStream = this.getRoomInstance().getEnemies().stream().parallel()
|
||||||
|
.filter(enemy -> !enemy.isDead())
|
||||||
|
.filter(enemy -> enemy.getPosition().distance(this.getPosition()) <= this.range);
|
||||||
|
return switch (this.priority.getType()) {
|
||||||
|
case PATH_DISTANCE -> {
|
||||||
|
List<Pos> path = this.getRoomInstance().getGame().getMazePath();
|
||||||
|
yield enemyStream
|
||||||
|
.sorted((o1, o2) -> Integer.compare(
|
||||||
|
pathProgress(o1, path),
|
||||||
|
pathProgress(o2, path)
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
case TOWER_DISTANCE -> enemyStream
|
||||||
|
.sorted((o1, o2) -> Double.compare(
|
||||||
|
o1.getPosition().distance(this.getPosition()),
|
||||||
|
o2.getPosition().distance(this.getPosition())
|
||||||
|
)).toList();
|
||||||
|
case HEALTH -> enemyStream
|
||||||
|
.sorted((o1, o2) -> Float.compare(o1.getHealth(), o2.getHealth()))
|
||||||
|
.toList();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int pathProgress(EntityCreature enemy, List<Pos> path) {
|
||||||
|
// Highest index == furthest along the path. Returned as negative so a
|
||||||
|
// natural ascending sort puts the most-advanced enemy first.
|
||||||
|
double bestDist = Double.MAX_VALUE;
|
||||||
|
int bestIdx = 0;
|
||||||
|
for(int i = 0; i < path.size(); i++) {
|
||||||
|
double d = enemy.getPosition().distanceSquared(path.get(i));
|
||||||
|
if(d < bestDist) {
|
||||||
|
bestDist = d;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -bestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TowerdefenseRoom getRoomInstance() {
|
||||||
|
return (TowerdefenseRoom) this.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSellingPrice(int buyPrice) {
|
||||||
|
return (int) (this.sellingPriceMultiplier * buyPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getValue() {
|
||||||
|
return this.damage * this.attacksPerSecond * this.range * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRange() {
|
||||||
|
return this.range;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void causeDamage(EntityCreature enemy) {
|
||||||
|
this.causeDamage(enemy, this.damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void causeDamage(EntityCreature enemy, float amount) {
|
||||||
|
enemy.damage(DamageType.PLAYER_ATTACK, amount);
|
||||||
|
if(enemy.isDead()) this.getRoomInstance().addMoney(Math.max(1, (int) enemy.getAttribute(Attribute.MAX_HEALTH).getBaseValue() / 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void attack(EntityCreature enemy);
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
|
||||||
|
public class WitherTower extends ShootingTower {
|
||||||
|
public WitherTower() {
|
||||||
|
super(EntityType.WITHER, 18, 4, 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.shootProjectile(enemy, EntityType.WITHER_SKULL, 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProjectileHit(EntityCreature enemy) {
|
||||||
|
super.onProjectileHit(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
|
||||||
|
|
||||||
|
import net.minestom.server.entity.EntityCreature;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.item.ItemStack;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
|
||||||
|
public class ZombieTower extends Tower {
|
||||||
|
public ZombieTower() {
|
||||||
|
super(EntityType.ZOMBIE, 7, 1, 4);
|
||||||
|
this.setItemInMainHand(ItemStack.of(Material.IRON_SWORD));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attack(EntityCreature enemy) {
|
||||||
|
this.swingMainHand();
|
||||||
|
this.causeDamage(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,4 +44,17 @@ public class Position {
|
|||||||
public static boolean hasPositionChanged(Pos oldPos, Pos newPos) {
|
public static boolean hasPositionChanged(Pos oldPos, Pos newPos) {
|
||||||
return !oldPos.withView(0, 0).equals(newPos.withView(0, 0));
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,8 @@ bungeecord:
|
|||||||
|
|
||||||
velocity:
|
velocity:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
# Chat nur über den Proxy verteilen, hier verwerfen (chat-bridge)
|
||||||
|
chat-via-proxy: false
|
||||||
secret: ''
|
secret: ''
|
||||||
|
|
||||||
api:
|
api:
|
||||||
@@ -19,3 +21,4 @@ api:
|
|||||||
admins:
|
admins:
|
||||||
- minetec
|
- minetec
|
||||||
- 28Pupsi28
|
- 28Pupsi28
|
||||||
|
- jannis_mg
|
||||||
@@ -190,7 +190,24 @@ description;Jump on the right color!;Springe auf die richtige Farbe!
|
|||||||
;;
|
;;
|
||||||
ns:game_PlayerRace#;;
|
ns:game_PlayerRace#;;
|
||||||
name;Player Race;Wettrennen
|
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#;;
|
ns:game_Backrooms#;;
|
||||||
name;Backrooms;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!
|
description;Escape the Backrooms before the others do - or be the last one alive!;Entkomme den Backrooms vor allen anderen - oder überlebe als Letzter!
|
||||||
|
|||||||
|
Reference in new issue
Block a user