diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java b/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java index a9a6a90..0c99f7e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java @@ -21,7 +21,6 @@ public enum Commands { ROOM(new RoomCommand()), UPDATE(new RefreshCommandsCommand()), OP(new OpCommand()), - FAKEPLAYER(new FakeplayerCommand()), KICK(new KickCommand()), SKIN(new SkinCommand()), SETOWNER(new SetRoomOwnerCommand()), @@ -41,7 +40,11 @@ public enum Commands { static { MinecraftServer.getCommandManager().setUnknownCommandCallback((sender, command) -> { if(command.isBlank()) return; - new ChatMessage(Icon.ERROR).appendStatic("Unknown command: ").quote(command).send(sender); + new ChatMessage(Icon.ERROR) + .appendTranslated("common#unknownCommand") + .appendSpace() + .quote(command) + .send(sender); }); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FakeplayerCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FakeplayerCommand.java deleted file mode 100644 index 69c31c5..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FakeplayerCommand.java +++ /dev/null @@ -1,31 +0,0 @@ -package eu.mhsl.minenet.minigames.command.privileged; - -import eu.mhsl.minenet.minigames.command.PrivilegedCommand; -import eu.mhsl.minenet.minigames.instance.room.Room; -import eu.mhsl.minenet.minigames.message.Icon; -import eu.mhsl.minenet.minigames.message.type.ChatMessage; -import net.minestom.server.command.builder.arguments.ArgumentType; -import net.minestom.server.entity.Player; - -import java.util.UUID; - -public class FakeplayerCommand extends PrivilegedCommand { - public FakeplayerCommand() { - super("fakeplayer"); - - addSyntax((sender, context) -> { - if(sender instanceof Player p) { - if(p.getInstance() instanceof Room room) { -// FakePlayer.initPlayer( // TODO FakePlayer does no longer exists -// UUID.randomUUID(), -// context.getRaw("name"), -// new FakePlayerOption().setInTabList(true).setRegistered(true), -// fakePlayer -> Room.setRoom(fakePlayer, room) -// ); - } else { - new ChatMessage(Icon.ERROR).appendStatic("Du musst dich in einer Raumlobby befinden!").send(sender); - } - } - }, ArgumentType.String("name")); - } -} diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java index 6a83853..85cf006 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java @@ -20,7 +20,7 @@ public class SetRoomOwnerCommand extends PrivilegedCommand { setDefaultExecutor((sender, context) -> { if(sender instanceof Player p) { Room.getRoom(p).orElseThrow().setOwner(p); - new ChatMessage(Icon.SUCCESS).appendStatic("You are now the owner of this room!").send(sender); + new ChatMessage(Icon.SUCCESS).appendTranslated("room#ownerSelf").send(sender); } }); @@ -29,7 +29,7 @@ public class SetRoomOwnerCommand extends PrivilegedCommand { if(sender instanceof Player p) { Player newOwner = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(context.getRaw("player")); Room.getRoom(p).orElseThrow().setOwner(Objects.requireNonNull(newOwner)); - new ChatMessage(Icon.SUCCESS).appendStatic("The new owner has been set!").send(sender); + new ChatMessage(Icon.SUCCESS).appendTranslated("room#ownerSet").send(sender); } }, ArgumentType.Entity("player").onlyPlayers(true)); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java b/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java index 72b17c0..2794283 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java @@ -7,7 +7,8 @@ import net.minestom.server.event.EventListener; public enum Listeners { SPAWN(new AddEntityToInstanceEventListener()), LOGIN(new PlayerLoginHandler()), - LEAVE(new PlayerLeaveHandler()); + LEAVE(new PlayerLeaveHandler()), + CHAT(new ChatFormatHandler()); Listeners(EventListener event) { MinecraftServer.getGlobalEventHandler().addListener(event); diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/global/ChatFormatHandler.java b/src/main/java/eu/mhsl/minenet/minigames/handler/global/ChatFormatHandler.java new file mode 100644 index 0000000..f548248 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/global/ChatFormatHandler.java @@ -0,0 +1,20 @@ +package eu.mhsl.minenet.minigames.handler.global; + +import eu.mhsl.minenet.minigames.message.Icon; +import eu.mhsl.minenet.minigames.message.type.ChatMessage; +import net.minestom.server.event.EventListener; +import net.minestom.server.event.player.PlayerChatEvent; +import org.jetbrains.annotations.NotNull; + +public class ChatFormatHandler implements EventListener { + @Override + public @NotNull Class eventType() { + return PlayerChatEvent.class; + } + + @Override + public @NotNull Result run(@NotNull PlayerChatEvent event) { + event.setFormattedMessage(new ChatMessage(Icon.CHAT).appendStatic(event.getRawMessage()).build(event.getPlayer())); + return Result.SUCCESS; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java index fcfc4bb..afa771e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java @@ -1,5 +1,9 @@ package eu.mhsl.minenet.minigames.handler.global; +import eu.mhsl.minenet.minigames.message.Icon; +import eu.mhsl.minenet.minigames.message.type.ChatMessage; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.minestom.server.event.EventListener; import net.minestom.server.event.player.PlayerDisconnectEvent; @@ -14,7 +18,11 @@ public class PlayerLeaveHandler implements EventListener @Override public @NotNull Result run(@NotNull PlayerDisconnectEvent event) { Player p = event.getPlayer(); -// new ChatMessage(Icon.SCIENCE).appendStatic("unübersetzter Leavetext: ").appendStatic(p.getDisplayName()).send(MinecraftServer.getConnectionManager().getOnlinePlayers()); + new ChatMessage(Icon.LEAVE) + .appendStatic(p.getName().color(NamedTextColor.GRAY)) + .appendSpace() + .appendTranslated("common#leave", NamedTextColor.DARK_GRAY) + .send(MinecraftServer.getConnectionManager().getOnlinePlayers()); return Result.SUCCESS; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java index b694668..fa8144f 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java @@ -65,8 +65,6 @@ public class PlayerLoginHandler implements EventListener> options, Player owner) { try { - - Game game = factory.manufacture(Room.getRoom(owner).orElseThrow(), options); + Room originRoom = Room.getRoom(owner).orElseThrow(); + Game game = factory.manufacture(originRoom, options); game.load(); - Room.getRoom(owner).orElseThrow().moveMembersToInstance(game); - - MinecraftServer.getSchedulerManager().scheduleTask(() -> { - game.getPlayers().forEach(player -> new ChatMessage(Icon.SCIENCE) - .appendStatic(factory.name().getAssembled(player).asComponent()) - .newLine() - .appendStatic(factory.description().getAssembled(player).asComponent()) - .send(player)); - - return TaskSchedule.stop(); - }, TaskSchedule.seconds(3)); + originRoom.moveMembersToInstance(game); + new ChatMessage(Icon.INFO) + .appendTranslated(factory.name()) + .newLine() + .appendTranslated(factory.description()) + .send(originRoom.getAllMembers()); } catch (Exception e) { new ChatMessage(Icon.ERROR).appendStatic("Instance crashed: " + e.getMessage()).send(owner); diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java index 150a603..628506c 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java @@ -13,6 +13,7 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge.Fastbr import eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround.HighGroundFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.jumpDive.JumpDiveFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake.SpaceSnakeFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.spleef.SpleefFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.stickfight.StickFightFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.TetrisFactory; @@ -20,6 +21,7 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo.SumoFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.tntrun.TntRunFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace.TrafficLightRaceFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame.TurtleGameFactory; public enum GameList { DEATHCUBE(new DeathcubeFactory(), GameType.JUMPNRUN), @@ -34,13 +36,15 @@ public enum GameList { TNTRUN(new TntRunFactory(), GameType.OTHER), ANVILRUN(new AnvilRunFactory(), GameType.PVE), ACIDRAIN(new AcidRainFactory(), GameType.PVE), + TURTLEGAME(new TurtleGameFactory(), GameType.PVE), ELYTRARACE(new ElytraRaceFactory(), GameType.PVP), SPLEEF(new SpleefFactory(), GameType.PVP), JUMPDIVE(new JumpDiveFactory(), GameType.JUMPNRUN), SUMO(new SumoFactory(), GameType.PVP), HIGHGROUND(new HighGroundFactory(), GameType.PVP), FASTBRIDGE(new FastbridgeFactory(), GameType.OTHER), - BLOCKBREAKRACE(new BlockBreakRaceFactory(), GameType.OTHER); + BLOCKBREAKRACE(new BlockBreakRaceFactory(), GameType.OTHER), + SPACESNAKE(new SpaceSnakeFactory(), GameType.PVP); private final GameFactory factory; private final GameType type; diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java index e318dd5..36aa3f1 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java @@ -57,7 +57,11 @@ public class StatelessGame extends Game { int timeLeft = timeLimit - timePlayed; switch (timeLeft) { case 90, 60, 30, 10, 5, 4, 3, 2, 1 -> - new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers()); + new ChatMessage(Icon.TIME) + .appendStatic(String.valueOf(timeLeft)) + .appendSpace() + .appendTranslated(timeLeft == 1 ? "common#secondsLeft_singular" : "common#secondsLeft_plural") + .send(getPlayers()); } timePlayed++; @@ -77,13 +81,23 @@ public class StatelessGame extends Game { * When overriding make sure to call this::start after countdown! */ protected CompletableFuture countdownStart() { - return new Countdown(TitleMessage.class) - .countdown(Audience.audience(getPlayers()), 5, countdownModifier -> countdownModifier.message = new TitleMessage(Duration.ofMillis(300), Duration.ofMillis(700)) - .subtitle(subtitleMessage -> subtitleMessage.appendStatic(Component.text("in ", NamedTextColor.DARK_GREEN)) - .appendStatic(Component.text(countdownModifier.timeLeft, NamedTextColor.GREEN)) - .appendStatic(Component.text(" seconds", NamedTextColor.DARK_GREEN)))); - } + Duration fadeIn = Duration.ofMillis(300); + Duration stay = Duration.ofMillis(700); + return new Countdown(TitleMessage.class) + .countdown( + Audience.audience(this.getPlayers()), + 5, + modifier -> modifier.message = new TitleMessage(fadeIn, stay) + .subtitle(subtitleMessage -> subtitleMessage + .appendTranslated("common#startIn", NamedTextColor.DARK_GREEN) + .appendSpace() + .appendStatic(Component.text(modifier.timeLeft, NamedTextColor.GREEN)) + .appendSpace() + .appendTranslated(modifier.timeLeft == 1 ? "common#second" : "common#seconds", NamedTextColor.DARK_GREEN) + ) + ); + } public void startAccessor() { this.start(); diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java index 9f7a446..c36725a 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java @@ -20,7 +20,10 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; -import net.minestom.server.event.player.*; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.event.player.PlayerStartFlyingWithElytraEvent; +import net.minestom.server.event.player.PlayerStopFlyingWithElytraEvent; +import net.minestom.server.event.player.PlayerUseItemEvent; import net.minestom.server.instance.batch.AbsoluteBlockBatch; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; @@ -49,80 +52,74 @@ public class ElytraRace extends StatelessGame { private final Material resetMaterial = Material.RED_DYE; private final int boostMultiplier = 50; private final Material ringMaterial = Material.GOLD_BLOCK; - private int generatedUntil = 0; - - private record CheckPointData(int currentCheckpoint, int nextCheckpoint) { - public CheckPointData next(int spacing) { - return new CheckPointData(nextCheckpoint, nextCheckpoint + spacing); - } - } private final Map playerCheckpoints = new HashMap<>(); + private int generatedUntil = 0; public ElytraRace(int ringCount) { super(Dimension.OVERWORLD.key, "ElytraRace", new FirstWinsScore()); this.ringCount = ringCount; - setGenerator(vale); - vale.setCalculateSeaLevel(point -> seaLevel); - vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1)); - vale.addMixIn(new PlaneTerrainGenerator(gameHeight, Block.BARRIER)); + this.setGenerator(this.vale); + this.vale.setCalculateSeaLevel(point -> this.seaLevel); + this.vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1)); + this.vale.addMixIn(new PlaneTerrainGenerator(this.gameHeight, Block.BARRIER)); - eventNode().addListener(PlayerUseItemEvent.class, playerUseItemEvent -> { + this.eventNode().addListener(PlayerUseItemEvent.class, playerUseItemEvent -> { Player player = playerUseItemEvent.getPlayer(); Material usedMaterial = playerUseItemEvent.getItemStack().material(); - if(usedMaterial.equals(boostMaterial)) { - if(!player.isFlyingWithElytra()) return; + if (usedMaterial.equals(this.boostMaterial)) { + if (!player.isFlyingWithElytra()) return; - boost(player); - InventoryUtil.removeItemFromPlayer(player, boostMaterial, 1); - } else if(usedMaterial.equals(resetMaterial)) { - toCheckpoint(player); - InventoryUtil.removeItemFromPlayer(player, resetMaterial, 1); + this.boost(player); + InventoryUtil.removeItemFromPlayer(player, this.boostMaterial, 1); + } else if (usedMaterial.equals(this.resetMaterial)) { + this.toCheckpoint(player); + InventoryUtil.removeItemFromPlayer(player, this.resetMaterial, 1); } }); - eventNode().addListener(PlayerStopFlyingWithElytraEvent.class, playerStopFlyingWithElytraEvent -> { + this.eventNode().addListener(PlayerStopFlyingWithElytraEvent.class, playerStopFlyingWithElytraEvent -> { Player player = playerStopFlyingWithElytraEvent.getPlayer(); - if(Position.blocksBelowPlayer(this, player).contains(ringMaterial.block())) { + if (Position.blocksBelowPlayer(this, player).contains(this.ringMaterial.block())) { player.setFlyingWithElytra(true); - boost(player); + this.boost(player); } else { - toCheckpoint(player); + this.toCheckpoint(player); // getScore().insertResult(playerStopFlyingWithElytraEvent.getPlayer()); // playerStopFlyingWithElytraEvent.getPlayer().setGameMode(GameMode.SPECTATOR); } }); - eventNode().addListener(PlayerStartFlyingWithElytraEvent.class, playerStartFlyingWithElytraEvent -> { - if(!isRunning) { + this.eventNode().addListener(PlayerStartFlyingWithElytraEvent.class, playerStartFlyingWithElytraEvent -> { + if (!this.isRunning) { playerStartFlyingWithElytraEvent.getPlayer().setFlyingWithElytra(false); return; } - boost(playerStartFlyingWithElytraEvent.getPlayer()); + this.boost(playerStartFlyingWithElytraEvent.getPlayer()); }); } @Override protected void onLoad(@NotNull CompletableFuture callback) { - Point spawnpoint = new Pos(vale.getXShiftAtZ(0), -46, 0); - GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> setBlock(point, BlockPallet.STREET.rnd())); + Point spawnpoint = new Pos(this.vale.getXShiftAtZ(0), -46, 0); + GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> this.setBlock(point, BlockPallet.STREET.rnd())); - generateRing(ringSpacing); - generateRing(ringSpacing * 2); + this.generateRing(this.ringSpacing); + this.generateRing(this.ringSpacing * 2); callback.complete(null); } @Override protected void onStart() { - getPlayers().forEach(player -> { + this.getPlayers().forEach(player -> { player.getInventory().setEquipment(EquipmentSlot.CHESTPLATE, (byte) 0, ItemStack.of(Material.ELYTRA)); - for(int i = 0; i < 3; i++) { - player.getInventory().setItemStack(i, ItemStack.builder(boostMaterial).customName(TranslatedComponent.byId("boost").getAssembled(player)).build()); + for (int i = 0; i < 3; i++) { + player.getInventory().setItemStack(i, ItemStack.builder(this.boostMaterial).customName(TranslatedComponent.byId("boost").getAssembled(player)).build()); } - addResetItemToPlayer(player); + this.addResetItemToPlayer(player); }); } @@ -131,44 +128,44 @@ public class ElytraRace extends StatelessGame { Player player = playerMoveEvent.getPlayer(); Point newPos = playerMoveEvent.getNewPosition(); - if(isBeforeBeginning && playerMoveEvent.getNewPosition().y() < getSpawn().y()) { - player.teleport(getSpawn()); + if (this.isBeforeBeginning && playerMoveEvent.getNewPosition().y() < this.getSpawn().y()) { + player.teleport(this.getSpawn()); return; } - playerCheckpoints.putIfAbsent(player, new CheckPointData(ringSpacing, ringSpacing * 2)); + this.playerCheckpoints.putIfAbsent(player, new CheckPointData(this.ringSpacing, this.ringSpacing * 2)); - if(newPos.z() > generatedUntil - ringSpacing) { - generateRing(generatedUntil + ringSpacing); + if (newPos.z() > this.generatedUntil - this.ringSpacing) { + this.generateRing(this.generatedUntil + this.ringSpacing); } - if(newPos.z() > playerCheckpoints.get(player).nextCheckpoint) { - playerCheckpoints.put(player, playerCheckpoints.get(player).next(ringSpacing)); - boost(player); + if (newPos.z() > this.playerCheckpoints.get(player).nextCheckpoint) { + this.playerCheckpoints.put(player, this.playerCheckpoints.get(player).next(this.ringSpacing)); + this.boost(player); } - if(newPos.y() > gameHeight - 5) { - Point particlePoint = newPos.withY(gameHeight); + if (newPos.y() > this.gameHeight - 5) { + Point particlePoint = newPos.withY(this.gameHeight); ParticlePacket particle = new ParticlePacket( - Particle.WAX_ON, - particlePoint.blockX(), - particlePoint.blockY(), - particlePoint.withZ(z -> z+10).blockZ(), - 20, - 0, - 30, - 1f, - Math.toIntExact((long) NumberUtil.map(newPos.y(), gameHeight - 5, gameHeight, 50, 500)) + Particle.WAX_ON, + particlePoint.blockX(), + particlePoint.blockY(), + particlePoint.withZ(z -> z + 10).blockZ(), + 20, + 0, + 30, + 1f, + Math.toIntExact((long) NumberUtil.map(newPos.y(), this.gameHeight - 5, this.gameHeight, 50, 500)) ); player.sendPacket(particle); } - if(getBlock(player.getPosition()).equals(Block.WATER)) { - toCheckpoint(player); + if (this.getBlock(player.getPosition()).equals(Block.WATER)) { + this.toCheckpoint(player); } - if(newPos.z() > ringCount * ringSpacing) { - getScore().insertResult(player); + if (newPos.z() > this.ringCount * this.ringSpacing) { + this.getScore().insertResult(player); player.setGameMode(GameMode.SPECTATOR); player.setFlyingWithElytra(false); } @@ -176,43 +173,44 @@ public class ElytraRace extends StatelessGame { @Override public Pos getSpawn() { - return new Pos(vale.getXShiftAtZ(0), -45, 0); + return new Pos(this.vale.getXShiftAtZ(0), -45, 0); } private void addResetItemToPlayer(Player p) { - p.getInventory().setItemStack(8, ItemStack.builder(resetMaterial).customName(TranslatedComponent.byId("reset").getAssembled(p)).build()); + p.getInventory().setItemStack(8, ItemStack.builder(this.resetMaterial).customName(TranslatedComponent.byId("reset").getAssembled(p)).build()); } private Point getRingPositionAtZ(int z) { Random random = new Random(this.hashCode() + z); - return new Pos(vale.getXShiftAtZ(z), -45 + random.nextInt(-5, 15), z); + return new Pos(this.vale.getXShiftAtZ(z), -45 + random.nextInt(-5, 15), z); } private CompletableFuture generateRing(int zPos) { - if(zPos > ringCount * ringSpacing) return null; - boolean isLast = (zPos == ringCount * ringSpacing); + if (zPos > this.ringCount * this.ringSpacing) return null; + boolean isLast = (zPos == this.ringCount * this.ringSpacing); - generatedUntil = zPos; + this.generatedUntil = zPos; AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - Point ringPos = getRingPositionAtZ(zPos); + Point ringPos = this.getRingPositionAtZ(zPos); GeneratorUtils.iterateArea( - ringPos.sub(100, 0, 0).withY(0), - ringPos.add(100, 0, 0).withY(seaLevel), - point -> batch.setBlock(point, Block.BARRIER) + ringPos.sub(100, 0, 0).withY(0), + ringPos.add(100, 0, 0).withY(this.seaLevel), + point -> batch.setBlock(point, Block.BARRIER) ); GeneratorUtils.iterateArea( - ringPos.sub(3, 3, 0), - ringPos.add(3, 3, 0), - point -> batch.setBlock(point, isLast ? Block.DIAMOND_BLOCK : ringMaterial.block()) + ringPos.sub(3, 3, 0), + ringPos.add(3, 3, 0), + point -> batch.setBlock(point, isLast ? Block.DIAMOND_BLOCK : this.ringMaterial.block()) ); GeneratorUtils.iterateArea( - ringPos.sub(2, 2, 0), - ringPos.add(2, 2, 0), - point -> batch.setBlock(point, Block.AIR) + ringPos.sub(2, 2, 0), + ringPos.add(2, 2, 0), + point -> batch.setBlock(point, Block.AIR) ); - BatchUtil.loadAndApplyBatch(batch, this, () -> {}); + BatchUtil.loadAndApplyBatch(batch, this, () -> { + }); return null; } @@ -221,13 +219,13 @@ public class ElytraRace extends StatelessGame { Vec playerVelocity = player.getPosition().direction(); player.setVelocity( - player.getVelocity().add(playerVelocity.mul(boostMultiplier)) - .withY(playerVelocity.withY(v -> v * boostMultiplier).y()) + player.getVelocity().add(playerVelocity.mul(this.boostMultiplier)) + .withY(playerVelocity.withY(v -> v * this.boostMultiplier).y()) ); } private void toCheckpoint(Player p) { - Point checkpointPos = getRingPositionAtZ(playerCheckpoints.get(p).currentCheckpoint); + Point checkpointPos = this.getRingPositionAtZ(this.playerCheckpoints.get(p).currentCheckpoint); p.setVelocity(Vec.ZERO); p.setFlyingWithElytra(false); p.teleport(Pos.fromPoint(checkpointPos).add(0.5, 0, 0.5)); @@ -236,27 +234,35 @@ public class ElytraRace extends StatelessGame { p.setFlyingSpeed(0); new Countdown(TitleMessage.class) - .countdown( - Audience.audience(p), - 3, - countdownModifier -> - countdownModifier.message = new TitleMessage( - Duration.ofMillis(300), - Duration.ofMillis(700) - ) - .subtitle( - subtitleMessage -> - subtitleMessage - .appendStatic(Component.text("Launch in ", NamedTextColor.DARK_GREEN)) - .appendStatic(Component.text(countdownModifier.timeLeft, NamedTextColor.GREEN)) - .appendStatic(Component.text(" seconds", NamedTextColor.DARK_GREEN)) - ) - ).thenRun(() -> { - p.setFlying(false); - p.setFlyingSpeed(1); - p.setFlyingWithElytra(true); - boost(p); - addResetItemToPlayer(p); - }); + .countdown( + Audience.audience(p), + 3, + countdownModifier -> + countdownModifier.message = new TitleMessage( + Duration.ofMillis(300), + Duration.ofMillis(700) + ) + .subtitle( + subtitleMessage -> + subtitleMessage + .appendTranslated("game_ElytraRace#launchIn") + .appendSpace() + .appendStatic(Component.text(countdownModifier.timeLeft, NamedTextColor.GREEN)) + .appendSpace() + .appendTranslated(countdownModifier.timeLeft == 1 ? "common#second" : "common#seconds") + ) + ).thenRun(() -> { + p.setFlying(false); + p.setFlyingSpeed(1); + p.setFlyingWithElytra(true); + this.boost(p); + this.addResetItemToPlayer(p); + }); + } + + private record CheckPointData(int currentCheckpoint, int nextCheckpoint) { + public CheckPointData next(int spacing) { + return new CheckPointData(this.nextCheckpoint, this.nextCheckpoint + spacing); + } } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java new file mode 100644 index 0000000..6e2ab4e --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java @@ -0,0 +1,182 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.PointsWinScore; +import eu.mhsl.minenet.minigames.util.MaterialUtil; +import io.github.togar2.pvp.events.FinalAttackEvent; +import io.github.togar2.pvp.events.PrepareAttackEvent; +import io.github.togar2.pvp.feature.CombatFeatures; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.*; +import net.minestom.server.entity.metadata.other.FallingBlockMeta; +import net.minestom.server.event.entity.EntityTickEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.WorldBorder; +import net.minestom.server.instance.block.Block; +import net.minestom.server.inventory.PlayerInventory; +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.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class SpaceSnake extends StatelessGame { + record PlayState(AtomicInteger length, Queue blocks, Material blockType, Pos spawn) { + public void cutToLength(Consumer removed) { + while (this.blocks.size() > this.length.get()) { + removed.accept(this.blocks.poll()); + } + } + } + + private final Map playerStates = new WeakHashMap<>(); + private int mapSize; + private final Supplier posInBoundsW = () -> this.rnd.nextInt(-this.mapSize/2, this.mapSize/2); + private final Supplier posInBoundsH = () -> this.rnd.nextInt(-60, 300); + + public SpaceSnake(int mapSize, int powerUpCount) { + super(Dimension.THE_END.key, "spaceSnake", new PointsWinScore()); + this.mapSize = mapSize; + this.setWorldBorder(new WorldBorder(this.mapSize, 0, 0, 0, 0)); + + for (int i = 0; i < powerUpCount; i++) { + this.spawnPowerUp(); + } + + this.eventNode().addChild( + CombatFeatures.empty() + .add(CombatFeatures.VANILLA_ATTACK) + .add(CombatFeatures.VANILLA_DAMAGE) + .add(CombatFeatures.VANILLA_KNOCKBACK) + .build() + .createNode() + ); + + this.eventNode().addListener(PrepareAttackEvent.class, prepareAttackEvent -> { + if (this.isBeforeBeginning) prepareAttackEvent.setCancelled(true); + }); + + this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> { + finalAttackEvent.setBaseDamage(0); + ((Player) finalAttackEvent.getTarget()).setHealth(20); + }); + } + + @Override + protected void onStart() { + this.getPlayers().forEach(player -> { + player.setGameMode(GameMode.SURVIVAL); + this.updateInv(player); + player.setHeldItemSlot((byte) 1); + }); + } + + @Override + protected void onStop() { + this.getPlayers().forEach(player -> this.getScore().insertResult(player, this.playerStates.get(player).length.get())); + } + + @Override + protected boolean onPlayerJoin(Player p) { + Pos spawn = new Pos(this.posInBoundsW.get(), -60, this.posInBoundsW.get()); + PlayState state = new PlayState( + new AtomicInteger(3), + new ArrayDeque<>(List.of(spawn)), + MaterialUtil.getRandomFullBlock(material -> !material.equals(Material.DIAMOND_BLOCK)), + spawn + ); + this.playerStates.put(p, state); + this.setBlock(spawn, state.blockType.block()); + MinecraftServer.getSchedulerManager().scheduleNextTick( + () -> p.teleport(this.getSaveSpawn(spawn)) + ); + return super.onPlayerJoin(p); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + PlayState state = this.playerStates.get(playerMoveEvent.getPlayer()); + if(this.isBeforeBeginning) { + boolean falling = state.blocks.stream().anyMatch(pos -> pos.y() > playerMoveEvent.getNewPosition().y()); + if(falling) playerMoveEvent.getPlayer().teleport(this.getSaveSpawn(state.spawn)); + return; + } + + if(playerMoveEvent.getNewPosition().y() < -64) { + this.getScore().insertResult(playerMoveEvent.getPlayer(), state.length.get()); + playerMoveEvent.getPlayer().teleport(this.getSpawn()); + playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR); + long livingPlayers = this.getPlayers().stream() + .filter(p -> this.getScore().hasResult(p)) + .count(); + if(livingPlayers == 1) this.setTimeLimit(10); + if(livingPlayers == 0) this.stop(); + } + } + + @Override + protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) { + if(this.isBeforeBeginning) { + playerBlockPlaceEvent.setCancelled(true); + return; + } + + PlayState state = this.playerStates.get(playerBlockPlaceEvent.getPlayer()); + state.blocks.add(playerBlockPlaceEvent.getBlockPosition().asVec().asPosition()); + state.cutToLength(pos -> this.setBlock(pos, Block.AIR)); + + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> this.updateInv(playerBlockPlaceEvent.getPlayer())); + playerBlockPlaceEvent.getPlayer().setLevel(state.length.get()); + } + + private Pos getSaveSpawn(Pos blockPos) { + return blockPos.add(0.5).withY((y) -> y + 2); + } + + private void updateInv(Player player) { + PlayerInventory inventory = player.getInventory(); + inventory.clear(); + inventory.addItemStack(ItemStack.of(Material.STICK, 1).with(builder -> builder.glowing(true))); + inventory.addItemStack(ItemStack.of(this.playerStates.get(player).blockType, 64)); + } + + private void spawnPowerUp() { + Pos spawnPos = new Pos(this.posInBoundsW.get(), this.posInBoundsH.get(), this.posInBoundsW.get()); + Entity display = new Entity(EntityType.FALLING_BLOCK); + ((FallingBlockMeta) display.getEntityMeta()).setBlock(Block.DIAMOND_BLOCK); + display.setGlowing(true); + display.setNoGravity(true); + display.setInstance(this, spawnPos); + + display.eventNode().addListener(EntityTickEvent.class, onTick -> { + Player player = this.getPlayers().stream() + .filter(p -> !this.getScore().hasResult(p)) + .filter(p -> p.getBoundingBox() + .grow(1, 1, 1) + .intersectBox(display.getPosition().sub(p.getPosition()), display.getBoundingBox()) + ) + .findAny() + .orElse(null); + if(player == null) return; + + this.spawnPowerUp(); + display.remove(); + this.onPowerup(player); + }); + } + + private void onPowerup(Player player) { + PlayState state = this.playerStates.get(player); + state.length.incrementAndGet(); + player.setLevel(player.getLevel() + 1); + player.playSound(Sound.sound(SoundEvent.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.PLAYER, 1f, 1f)); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnakeFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnakeFactory.java new file mode 100644 index 0000000..97a17a5 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnakeFactory.java @@ -0,0 +1,41 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake; + +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 SpaceSnakeFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_SpaceSnake#name"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("width", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#width"), 20, 30, 40, 50, 60, 70, 80)) + .addOption(new NumericOption("powerUpCount", Material.DIAMOND, TranslatedComponent.byId("game_SpaceSnake#powerUpCount"), 50, 100, 200, 300)); + } + + @Override + public Material symbol() { + return Material.GREEN_CONCRETE_POWDER; + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_SpaceSnake#description"); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new SpaceSnake(configuration.get("width").getAsInt(), configuration.get("powerUpCount").getAsInt()).setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java index f3be090..5fc1b12 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java @@ -28,7 +28,7 @@ public class StickFightFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("length", Material.SANDSTONE, TranslatedComponent.byId("optionCommon#length"), 5, 7, 9, 11)); + .addOption(new NumericOption("length", Material.SANDSTONE, TranslatedComponent.byId("optionCommon#length"), 7, 10, 13, 16, 19)); } @Override @@ -40,7 +40,7 @@ public class StickFightFactory implements GameFactory { @Override public Game manufacture(Room parent, Map> configuration) { - return new Stickfight().setParent(parent); + return new Stickfight(configuration.get("length").getAsInt()).setParent(parent); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java index 26709ec..5afb4c6 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java @@ -18,12 +18,14 @@ import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; public class Stickfight extends StatelessGame { - private final double radius = 20; + private final double radius; private final WeakHashMap spawnPoints = new WeakHashMap<>(); private final Map scoreMap = new WeakHashMap<>(); + private boolean countdownStarted = false; - public Stickfight() { + public Stickfight(int length) { super(Dimension.OVERWORLD.key, "Stickfight", new LowestPointsWinScore()); + this.radius = length; eventNode().addChild( CombatFeatures.empty() @@ -34,6 +36,7 @@ public class Stickfight extends StatelessGame { ); eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> { + if(isBeforeBeginning) finalAttackEvent.setCancelled(true); finalAttackEvent.setBaseDamage(0); ((Player) finalAttackEvent.getTarget()).setHealth(20); }); @@ -43,14 +46,26 @@ public class Stickfight extends StatelessGame { @Override protected void onLoad(@NotNull CompletableFuture callback) { - setBlock(0, 50, 0, Block.DIAMOND_BLOCK); + this.replaceCircle(Block.SANDSTONE); + } + + private void replaceCircle(Block block) { + int radius = 8; + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + Pos blockPosition = this.getSpawn().add(x, -1, z); + if(blockPosition.distance(this.getSpawn().sub(0, 1, 0)) <= radius) this.setBlock(blockPosition, block); + } + } } @Override protected void start() { List players = getPlayers().stream().toList(); int numPlayers = players.size(); + this.countdownStarted = true; + this.replaceCircle(Block.AIR); for (int i = 0; i < numPlayers; i++) { double angle = (2 * Math.PI / numPlayers) * i; int spawnX = (int) (radius * Math.cos(angle)); @@ -87,7 +102,8 @@ public class Stickfight extends StatelessGame { protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { Player player = playerMoveEvent.getPlayer(); if(!spawnPoints.containsKey(player)) { - playerMoveEvent.setCancelled(true); + if(playerMoveEvent.getNewPosition().y() < 45) player.teleport(this.getSpawn()); + if(this.countdownStarted) playerMoveEvent.setCancelled(true); return; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGame.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGame.java new file mode 100644 index 0000000..c2ab1b3 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGame.java @@ -0,0 +1,254 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame; + +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.turtleGame.gameObjects.Turtle; +import eu.mhsl.minenet.minigames.score.PointsWinScore; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.*; +import net.minestom.server.entity.metadata.other.FallingBlockMeta; +import net.minestom.server.event.player.PlayerStartSneakingEvent; +import net.minestom.server.event.player.PlayerStopSneakingEvent; +import net.minestom.server.event.player.PlayerTickEvent; +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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +class TurtleGame extends StatelessGame { + private final int radius; + private final Map turtlePlayerMap = new WeakHashMap<>(); + private final ArrayList snacks = new ArrayList<>(); + private final ArrayList bombs = new ArrayList<>(); + private final Block snackBlock = Block.SUNFLOWER.withProperty("half", "upper"); + private final double startSpeed; + + public TurtleGame(int radius, int startSpeed) { + super(Dimension.OVERWORLD.key, "Turtle Game", new PointsWinScore()); + this.radius = radius; + this.startSpeed = startSpeed; + + this.eventNode() + .addListener(PlayerTickEvent.class, this::onPlayerTick) + .addListener(PlayerStartSneakingEvent.class, this::onPlayerStartSneak) + .addListener(PlayerStopSneakingEvent.class, this::onPlayerStopSneak); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + for(int x = -this.radius - 1; x <= this.radius + 1; x++) { + for(int z = -this.radius - 1; z <= this.radius + 1; z++) { + double distance = new Pos(x, 0, z).distance(new Pos(0, 0, 0)); + if(distance <= this.radius) { + boolean isEdge = this.radius - 1 < distance; + this.setBlock(x, 0, z, Block.SAND); + this.setBlock(x, 1, z, isEdge ? Block.STONE : Block.AIR); + this.setBlock(x, 2, z, isEdge ? Block.STONE_SLAB : Block.AIR); + } else { + this.setBlock(x, 0, z, Block.AIR); + } + } + } + } + + private void onPlayerStartSneak(@NotNull PlayerStartSneakingEvent event) { + Player p = event.getPlayer(); + this.turtlePlayerMap.get(p).boostSpeed(); + } + + private void onPlayerStopSneak(@NotNull PlayerStopSneakingEvent event) { + Player p = event.getPlayer(); + this.turtlePlayerMap.get(p).cancelBoost(); + } + + protected void onPlayerTick(@NotNull PlayerTickEvent event) { + Player p = event.getPlayer(); + if(p.getGameMode() == GameMode.SPECTATOR) return; + Turtle turtle = this.turtlePlayerMap.get(p); + turtle.adaptView(); + if(this.isRunning()) { + turtle.move(); + this.snacks.stream() + .filter(turtle::checkCollisionWithEntity) + .toList() + .forEach(snack -> { + this.eat(p, snack); + this.generateNewSnack(); + if(this.turtlePlayerMap.get(p).getScore() % 5 == 0) { + this.generateNewBomb(); + this.addSpeed(0.4, p); + } + }); + this.bombs.stream() + .filter(turtle::checkCollisionWithEntity) + .toList() + .forEach(bomb -> { + this.explode(p, bomb); + this.generateNewBombs(2); + this.addGlobalSpeed(0.3); + }); + } + } + + protected void addSpeed(double amount, Player p) { + this.turtlePlayerMap.get(p).addSpeed(amount); + } + + protected void addGlobalSpeed(double amount) { + this.turtlePlayerMap.values().forEach(turtle -> turtle.addSpeed(amount)); + } + + protected void eat(Player p, Entity snack) { + p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EAT, Sound.Source.MASTER, 1f, 1f), snack.getPosition()); + Material snackMaterial = Objects.requireNonNull(this.snackBlock.registry().material()); + p.sendPacket(new ParticlePacket(Particle.ITEM.withItem(ItemStack.of(snackMaterial)), p.getPosition(), new Pos(0.5, 0.5, 0.5), 0, 8)); + this.snacks.remove(snack); + snack.remove(); + this.turtlePlayerMap.get(p).increaseScore(); + this.turtlePlayerMap.get(p).increaseBoostChargeLevel(0.04f); + } + + protected void explode(Player p, Entity bomb) { + this.letPlayerLoose(p); + p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.MASTER, 2f, 1f), bomb.getPosition()); + p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.MASTER, 2f, 1f), bomb.getPosition()); + p.sendPacket(new ParticlePacket(Particle.EXPLOSION, p.getPosition(), new Pos(0.5, 0.5, 0.5), 0, 8)); + if(this.getLeftPlayers().size() == 1) this.setTimeLimit(10); + this.bombs.remove(bomb); + bomb.remove(); + } + + protected void letPlayerLoose(Player p) { + p.setGameMode(GameMode.SPECTATOR); + p.setFlying(true); + this.turtlePlayerMap.get(p).destroy(); + this.getScore().insertResult(p, this.turtlePlayerMap.get(p).getScore()); + } + + protected List getLeftPlayers() { + return this.turtlePlayerMap.keySet().stream() + .filter(player -> !this.getScore().hasResult(player)) + .toList(); + } + + @Override + protected boolean onPlayerJoin(Player p) { + this.turtlePlayerMap.putIfAbsent(p, new Turtle(p, this.startSpeed)); + p.setLevel(this.turtlePlayerMap.get(p).getScore()); + + Turtle turtle = this.turtlePlayerMap.get(p); + MinecraftServer.getSchedulerManager().scheduleNextTick(turtle::spawnTurtle); + return super.onPlayerJoin(p); + } + + @Override + protected void onPlayerLeave(Player p) { + Turtle turtle = this.turtlePlayerMap.get(p); + turtle.remove(); + } + + @Override + public Pos getSpawn() { + double theta = this.rnd.nextDouble() * 2 * Math.PI; + + double spawnRadius = this.radius - 4; + double x = spawnRadius * Math.cos(theta); + double z = spawnRadius * Math.sin(theta); + + return new Pos(x, 1, z).withLookAt(new Pos(0, 0, 0)); + } + + private void generateNewSnacks(int count) { + for (int i = 0; i < count; i++) { + this.generateNewSnack(); + } + } + + private void generateNewSnack() { + Entity snack = new Entity(EntityType.FALLING_BLOCK); + FallingBlockMeta meta = (FallingBlockMeta) snack.getEntityMeta(); + meta.setBlock(this.snackBlock); + meta.setCustomName(Component.text("Snack")); + meta.setCustomNameVisible(true); + Pos spawnPosition = this.newSpawnPosition(snack); + if(spawnPosition == null) { + snack.remove(); + return; + } + snack.setInstance(this, spawnPosition); + this.snacks.add(snack); + } + + private void generateNewBombs(int count) { + for (int i = 0; i < count; i++) { + this.generateNewBomb(); + } + } + + private void generateNewBomb() { + Entity bomb = new Entity(EntityType.FALLING_BLOCK); + FallingBlockMeta meta = (FallingBlockMeta) bomb.getEntityMeta(); + meta.setBlock(Block.TNT); + meta.setCustomName(Component.text("Bomb").color(NamedTextColor.RED).decorate(TextDecoration.BOLD)); + meta.setCustomNameVisible(true); + Pos spawnPosition = this.newSpawnPosition(bomb, false); + if(spawnPosition == null) { + bomb.remove(); + return; + } + bomb.setInstance(this, spawnPosition); + this.bombs.add(bomb); + } + + private @Nullable Pos newSpawnPosition(Entity entity) { + return this.newSpawnPosition(entity, true); + } + + private @Nullable Pos newSpawnPosition(Entity entity, boolean nearPlayers) { + Pos spawnPosition; + int counter = 0; + boolean isInRadius, collides; + do { + if(counter > 200) return null; + int x = this.rnd.nextInt(-this.radius+2, this.radius-2); + int z = this.rnd.nextInt(-this.radius+2, this.radius-2); + spawnPosition = new Pos(x, 1, z).add(0.5, 0, 0.5); + Pos checkPosition = spawnPosition; + isInRadius = checkPosition.distance(0, 1, 0) < this.radius-2; + collides = this.getEntities().stream() + .filter(e -> !e.equals(entity)) + .anyMatch(e -> entity.getBoundingBox().intersectBox(e.getPosition().sub(checkPosition), e.getBoundingBox())); + if(!collides && !nearPlayers) collides = this.turtlePlayerMap.values().stream() + .filter(turtle -> !turtle.equals(entity)) + .anyMatch(turtle -> entity.getBoundingBox() + .growSymmetrically(turtle.getBombBorder(), 1, turtle.getBombBorder()) + .intersectBox(turtle.getPosition().sub(checkPosition), turtle.getBoundingBox())); + counter++; + } while (!isInRadius || collides); + return spawnPosition; + } + + @Override + protected void onStart() { + this.generateNewSnacks(this.turtlePlayerMap.size() + 1); + this.generateNewBombs((int) Math.ceil(this.snacks.size() * 0.5)); + this.turtlePlayerMap.values().forEach(Turtle::startBoostRefill); + } + + @Override + protected void onStop() { + this.getLeftPlayers().forEach(this::letPlayerLoose); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGameFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGameFactory.java new file mode 100644 index 0000000..1f66cf5 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGameFactory.java @@ -0,0 +1,42 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame; + +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 TurtleGameFactory implements GameFactory { + + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_TurtleGame#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_TurtleGame#description"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30, 40)) + .addOption(new NumericOption("startSpeed", Material.LEATHER_BOOTS, TranslatedComponent.byId("game_TurtleGame#startSpeed"), 2, 3, 4, 6, 8)); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new TurtleGame(configuration.get("radius").getAsInt(), configuration.get("startSpeed").getAsInt()).setParent(parent); + } + + @Override + public Material symbol() { + return Material.TURTLE_EGG; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/gameObjects/Turtle.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/gameObjects/Turtle.java new file mode 100644 index 0000000..5ba3916 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/gameObjects/Turtle.java @@ -0,0 +1,110 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame.gameObjects; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.timer.Task; +import net.minestom.server.timer.TaskSchedule; + +public class Turtle extends EntityCreature { + private final Player player; + private int score = 0; + private double speed; + private double boostSpeedMultiplier = 1; + private float boostChargeLevel = 0f; + private Task boostTask; + private Task boostRefillTask; + + public Turtle(Player player, double speed) { + super(EntityType.TURTLE); + this.player = player; + this.speed = speed; + + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.15); + this.getAttribute(Attribute.MAX_HEALTH).setBaseValue(1); + this.player.setExp(this.boostChargeLevel); + } + + public void spawnTurtle() { + this.setInstance(this.player.getInstance(), this.player.getPosition()); + this.addPassenger(this.player); + } + + public void startBoostRefill() { + this.boostRefillTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> { + if(this.boostChargeLevel >= 1f) return; + this.increaseBoostChargeLevel(0.02f); + }, TaskSchedule.seconds(1), TaskSchedule.seconds(1)); + } + + public void destroy() { + this.removePassenger(this.player); + this.remove(); + if(this.boostRefillTask != null && this.boostRefillTask.isAlive()) this.boostRefillTask.cancel(); + if(this.boostTask != null && this.boostTask.isAlive()) this.boostTask.cancel(); + } + + public void adaptView() { + if(this.getInstance() == null) return; + this.teleport(this.getPosition().withView(this.player.getPosition().withPitch(this.getPosition().pitch()))); + Vec lookingVector = this.player.getPosition().direction().withY(0).mul(100); + this.lookAt(this.getPosition().add(lookingVector.asPosition())); + } + + public void move() { + this.adaptView(); + Vec direction = this.player.getPosition().direction(); + Vec movementVector = direction.withY(0).normalize().mul(this.speed * this.boostSpeedMultiplier); + this.setVelocity(movementVector); + } + + public boolean checkCollisionWithEntity(Entity other) { + return this.getBoundingBox().intersectBox(other.getPosition().sub(this.getPosition()), other.getBoundingBox()); + } + + public void boostSpeed() { + if(this.boostChargeLevel <= 0f) return; + this.boostSpeedMultiplier = 3.5; + this.boostTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> { + if(this.boostChargeLevel <= 0f) { + this.cancelBoost(); + return; + } + this.boostChargeLevel = Math.max(0f, this.boostChargeLevel - 0.025f); + this.player.setExp(this.boostChargeLevel); + }, TaskSchedule.millis(30), TaskSchedule.millis(30)); + } + + public void cancelBoost() { + if(this.boostTask == null || !this.boostTask.isAlive()) return; + this.boostTask.cancel(); + this.boostSpeedMultiplier = 1; + } + + public void increaseBoostChargeLevel(float amount) { + this.boostChargeLevel = Math.min(1f, this.boostChargeLevel + amount); + this.player.setExp(this.boostChargeLevel); + } + + public void addSpeed(double amount) { + this.speed += amount; + } + + public int getScore() { + return this.score; + } + + public double getBombBorder() { + // 1 bei speed 2; 2 bei speed 4; 4 bei speed 8 + return Math.clamp((this.speed * this.boostSpeedMultiplier) / 2, 1.5, 4); + } + + public void increaseScore() { + this.score += 1; + this.player.setLevel(this.score); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java index 467edcb..4244894 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java @@ -55,7 +55,11 @@ public class JoinInventory extends InteractableInventory { if(target.isPresent()) Room.setRoom(player, target.get()); else - new ChatMessage(Icon.ERROR).appendTranslated("hub#join_notFound").appendStatic(" " + typedText).send(player); + new ChatMessage(Icon.ERROR) + .appendTranslated("hub#join_notFound") + .appendSpace() + .quote(typedText.trim()) + .send(player); } private String formatInput(String raw) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java b/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java index 6b525f3..cc2f0df 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java @@ -135,9 +135,17 @@ public class Room extends MineNetInstance implements Spawnable { Room.unsetRoom(p); - new ChatMessage(Icon.ERROR).appendStatic("The room leader left!").send(this.getAllMembers()); - new ChatMessage(Icon.SCIENCE).appendStatic(this.owner.getUsername()).appendStatic(" is the new Leader!").send(this.getAllMembers().stream().filter(player -> player != this.owner).collect(Collectors.toSet())); - new ChatMessage(Icon.SUCCESS).appendStatic("You are now the leader.").send(this.owner); + new ChatMessage(Icon.ERROR) + .appendTranslated("room#ownerLeft") + .send(this.getAllMembers()); + new ChatMessage(Icon.SCIENCE) + .appendStatic(this.owner.getUsername()) + .appendSpace() + .appendTranslated("room#newOwnerAnnounce") + .send(this.getAllMembers().stream().filter(player -> player != this.owner).collect(Collectors.toSet())); + new ChatMessage(Icon.SUCCESS) + .appendTranslated("room#ownerSelf") + .send(this.owner); }); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java b/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java index 7603fe7..8ad6ac9 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java @@ -37,7 +37,9 @@ public class GameSelector extends InteractableEntity { if(playerEntityInteractEvent.getPlayer() != room.getOwner()) { abstractVillagerMeta.setHeadShakeTimer(20); - new ChatMessage(Icon.ERROR).appendStatic("Only the room leader can start games!").send(playerEntityInteractEvent.getPlayer()); + new ChatMessage(Icon.ERROR) + .appendTranslated("room#onlyOwnerCanStart") + .send(playerEntityInteractEvent.getPlayer()); return; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java b/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java index 66bf0bb..0c70f86 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java +++ b/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java @@ -53,7 +53,7 @@ public class Languages { for(String line : Files.readAllLines(locale.toPath())) { //line = line.replaceAll("[^\\p{L}\\s,#_+.:;]+", ""); - line = line.replaceAll("[^a-zA-Z0-9äöüÄÖÜ ,:;#_+]", ""); + line = line.replaceAll("[^a-zA-Z0-9äöüÄÖÜ ,:;#!_+]", ""); String[] columns = line.split(";"); if(columns.length < 1) continue; diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java b/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java index 35efac2..0879e21 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java @@ -8,7 +8,10 @@ public enum Icon { STAR("\u2606", NamedTextColor.GOLD), CHAT("\u276F\u276F", NamedTextColor.WHITE), SUCCESS("\u2714", NamedTextColor.GREEN), - ERROR("\u274C", NamedTextColor.RED); + ERROR("\u274C", NamedTextColor.RED), + TIME("\u231B", NamedTextColor.YELLOW), + INFO("\uD83D\uDD14", NamedTextColor.AQUA), + LEAVE("\u2B05", NamedTextColor.DARK_GRAY); private final String symbol; private final NamedTextColor color; diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java b/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java index 855ee5c..a485896 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java @@ -31,8 +31,23 @@ public abstract class TranslatableMessage implements Sendable { return this; } + public TranslatableMessage appendSpace() { + chain.add(Component.text(" ")); + return this; + } + public TranslatableMessage appendTranslated(String mapId) { - chain.add(TranslatedComponent.byId(mapId)); + chain.add(TranslatedComponent.byId(mapId).setColor(NamedTextColor.WHITE)); + return this; + } + + public TranslatableMessage appendTranslated(String mapId, NamedTextColor color) { + chain.add(TranslatedComponent.byId(mapId).setColor(color)); + return this; + } + + public TranslatableMessage appendTranslated(TranslatedComponent component) { + chain.add(component); return this; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java index 6effaf9..3fbfa90 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java @@ -1,9 +1,28 @@ package eu.mhsl.minenet.minigames.util; +import net.minestom.server.instance.block.BlockFace; import net.minestom.server.item.Material; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Predicate; + public class MaterialUtil { public static Material fromString(String name, Material def) { return Material.values().stream().filter(material -> material.name().equals(name)).findFirst().orElse(def); } + + public static Material getRandomFullBlock(Predicate filter) { + List blocks = Material.values().stream() + .filter(filter) + .filter(Material::isBlock) + .filter(material -> material.block().isSolid()) + .filter(material -> Arrays.stream(BlockFace.values()) + .allMatch(face -> material.block().registry().collisionShape().isFaceFull(face)) + ) + .toList(); + + return blocks.get(ThreadLocalRandom.current().nextInt(blocks.size())); + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java b/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java index 483d893..9dde82b 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java @@ -12,7 +12,7 @@ public enum BlockPallet { STONE(new Block[] {Block.CHISELED_STONE_BRICKS, Block.STONE_BRICKS, Block.POLISHED_ANDESITE, Block.POLISHED_BLACKSTONE, Block.POLISHED_DIORITE}), WINTER(new Block[] {Block.SNOW_BLOCK, Block.ICE, Block.PACKED_ICE, Block.BLUE_CONCRETE, Block.SEA_LANTERN}), STREET(new Block[] {Block.BLACK_CONCRETE_POWDER, Block.GRAY_CONCRETE_POWDER, Block.GRAVEL, Block.BLACK_CONCRETE, Block.GRAY_CONCRETE}), - FLOWER(new Block[] {Block.ORANGE_TULIP, Block.PINK_TULIP, Block.RED_TULIP, Block.WHITE_TULIP}), + FLOWER(new Block[] {Block.ORANGE_TULIP, Block.PINK_TULIP, Block.RED_TULIP, Block.WHITE_TULIP, Block.DANDELION, Block.POPPY, Block.CORNFLOWER}), PRESSURE_PLATES(new Block[] {Block.ACACIA_PRESSURE_PLATE, Block.BIRCH_PRESSURE_PLATE, Block.CRIMSON_PRESSURE_PLATE, Block.JUNGLE_PRESSURE_PLATE, Block.OAK_PRESSURE_PLATE, Block.DARK_OAK_PRESSURE_PLATE, Block.HEAVY_WEIGHTED_PRESSURE_PLATE, Block.HEAVY_WEIGHTED_PRESSURE_PLATE, Block.POLISHED_BLACKSTONE_PRESSURE_PLATE, Block.SPRUCE_PRESSURE_PLATE, Block.STONE_PRESSURE_PLATE, Block.WARPED_PRESSURE_PLATE}); final List list; diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java index 362d3fe..a3a1c08 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java @@ -55,7 +55,19 @@ public class HeightTerrainGenerator extends BaseGenerator { synchronized (batches) { double batchNoise = batches.getNoise(bottomPoint.x(), bottomPoint.z()); - unit.modifier().fill(bottomPoint, bottomPoint.add(1, heightModifier, 1), batchNoise < 0.9 ? batchNoise > 0 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE); + Block block = batchNoise < 0.9 ? batchNoise > -0.2 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE; + unit.modifier().fill(bottomPoint, bottomPoint.add(1, heightModifier, 1), block); + if(rnd.nextInt(0, 5) < 1 && block == Block.GRASS_BLOCK) { + int randomInt = rnd.nextInt(0, 6); + if(randomInt > 1) { + unit.modifier().setBlock(bottomPoint.add(0, heightModifier, 0), Block.SHORT_GRASS); + } else if(randomInt > 0) { + unit.modifier().setBlock(bottomPoint.add(0, heightModifier, 0), BlockPallet.FLOWER.rnd()); + } else { + unit.modifier().setBlock(bottomPoint.add(0, heightModifier, 0), Block.TALL_GRASS); + unit.modifier().setBlock(bottomPoint.add(0, heightModifier+1, 0), Block.TALL_GRASS.withProperty("half", "upper")); + } + } if(calculateSeaLevel != null) { Point absoluteHeight = bottomPoint.add(0, heightModifier, 0); diff --git a/src/main/resources/lang/locales.map.csv b/src/main/resources/lang/locales.map.csv index d2167ae..135c512 100644 --- a/src/main/resources/lang/locales.map.csv +++ b/src/main/resources/lang/locales.map.csv @@ -9,6 +9,13 @@ select_language;Please select your prefered Language;Bitte wähle deine bevorzug welcome;Welcome!;Willkommen! back;Back;Zurück forward;Next;Nächste +leave;left the game;hat das Spiel verlassen +secondsLeft_singular;second left;Sekunde verbleibt +secondsLeft_plural;seconds left;Sekunden verbleiben +unknownCommand;Unknown command;Unbekannter Befehl +startIn;Start in;Startet in +second;second;Sekunde +seconds;seconds;Sekunden ;; ns:tablist#UNUSED;; title;MineNet Network;MineNet Servernetzwerk @@ -65,6 +72,11 @@ ns:room#;; invTitle;Select a Minigame;Wähle einen Spielmodus noOption;No options here;Keine Optionen hier noOptionDescription;There are no options for this Game;Es gibt keine Einstellungen für dieses Spiel +ownerSelf;You are now the owner of this room!;Du bist nun Besitzer dieser Lobby! +ownerSet;The new owner has been set!;Der neue Besitzer wurde festgelegt! +ownerLeft;The room owner has left!;Der Lobbybesitzer hat das Spiel verlassen! +newOwnerAnnounce;is the new owner!;ist der neue Besitzer +onlyOwnerCanStart;Only the room leader can start games!;Nur der Raumbesitzer kann Spiele starten! ;; ns:GameFactory#;; missingDescription;No description;Keine Beschreibung @@ -78,6 +90,7 @@ ns:game_ElytraRace#;; name;Elytra race;Elytra Rennen description;Be fast while flying through the rings;Sei schnell während du durch die Ringe fliegst ringCount;ring count;Anzahl der Ringe +launchIn;Launch in;Abschuss in ;; ns:game_Minerun#;; name;Minerun;Minenrennen @@ -147,4 +160,13 @@ description;Speedbridge to the other platform. The first one there wins!;Baue di ns:game_BlockBreakRace#;; name;Block Break Race;Blockbruch-Rennen description;Dig down through the tubes using the right tools. The first player to reach the bottom wins!;Grabe dich durch die Röhren nach unten und verwende dabei das richtige Werkzeug. Wer zuerst unten ankommt, gewinnt! -;; \ No newline at end of file +;; +ns:game_SpaceSnake#;; +name;Space Snake;Weltraum-Snake +description;Collect diamonds while extending your snake bridge through space and fight other players. The player with the longest bridge wins!;Sammle Diamanten, während du deine Schlangenbrücke durchs All erweiterst und gegen andere Spieler kämpfst. Der Spieler mit den der Längsten Brücke gewinnt! +powerUpCount;Number of diamonds in the arena;Anzahl der Diamanten in der Arena +;; +ns:game_TurtleGame#;; +name;Turtle Game;Turtle Game +description;Eat snacks and dodge bombs to get the highest score!;Esse Snacks und weiche Bomben aus, um den höchsten Score zu erreichen! +startSpeed;Start Speed;Startgeschwindigkeit