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 98943df..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 @@ -21,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), @@ -35,6 +36,7 @@ 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), 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/resources/lang/locales.map.csv b/src/main/resources/lang/locales.map.csv index 749dbe8..5de9337 100644 --- a/src/main/resources/lang/locales.map.csv +++ b/src/main/resources/lang/locales.map.csv @@ -152,3 +152,8 @@ 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