From 5bb07596a176e355e95158bb70d85835273e7b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 15 Oct 2025 00:42:11 +0200 Subject: [PATCH] added SpaceSnake game and related assets --- .../minigames/instance/game/GameList.java | 4 +- .../types/spaceSnake/SpaceSnake.java | 181 ++++++++++++++++++ .../types/spaceSnake/SpaceSnakeFactory.java | 41 ++++ src/main/resources/lang/locales.map.csv | 6 +- 4 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java create mode 100644 src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnakeFactory.java 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..c7c3651 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; @@ -40,7 +41,8 @@ public enum GameList { 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), + SNAKE3D(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/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..2411903 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java @@ -0,0 +1,181 @@ +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 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.display.BlockDisplayMeta; +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.instance.block.BlockFace; +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 playerBlocks = 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(-32, 100); + + 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 boolean onPlayerJoin(Player p) { + Pos spawn = new Pos(this.posInBoundsW.get(), 50, this.posInBoundsW.get()); + PlayState state = new PlayState( + new AtomicInteger(3), + new ArrayDeque<>(List.of(spawn)), + this.getRandomBlock(), + spawn + ); + this.playerBlocks.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.playerBlocks.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); + } + } + + @Override + protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) { + if(this.isBeforeBeginning) { + playerBlockPlaceEvent.setCancelled(true); + return; + } + + PlayState state = this.playerBlocks.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.playerBlocks.get(player).blockType, 64)); + } + + private Material getRandomBlock() { + List blocks = Material.values().stream() + .filter(Material::isBlock) + .filter(material -> material.registry().block() != null) + .filter(material -> material.block().isSolid()) + .filter(material -> Arrays.stream(BlockFace.values()) + .allMatch(face -> material.block().registry().collisionShape().isFaceFull(face)) + ) + .toList(); + return blocks.get(this.rnd.nextInt(blocks.size())); + } + + private void spawnPowerUp() { + Pos spawnPos = new Pos(this.posInBoundsW.get(), this.posInBoundsH.get(), this.posInBoundsW.get()); + Entity display = new Entity(EntityType.BLOCK_DISPLAY); + ((BlockDisplayMeta) display.getEntityMeta()).setBlockState(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.getDistance(onTick.getEntity()) < 2.5) + .findAny() + .orElse(null); + if(player == null) return; + + this.spawnPowerUp(); + display.remove(); + this.onPowerup(player); + }); + } + + private void onPowerup(Player player) { + PlayState state = this.playerBlocks.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/resources/lang/locales.map.csv b/src/main/resources/lang/locales.map.csv index d2167ae..749dbe8 100644 --- a/src/main/resources/lang/locales.map.csv +++ b/src/main/resources/lang/locales.map.csv @@ -147,4 +147,8 @@ 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