21 Commits

Author SHA1 Message Date
d5910b4b54 renamed methods 2025-10-06 17:40:08 +02:00
ec76dd5c85 added boost charge when eating snacks 2025-10-06 17:31:04 +02:00
84de61388e improved speed mechanic and bomb spawning, added countdown for last player 2025-10-05 18:24:46 +02:00
dece9c13b7 added translations 2025-10-05 00:19:37 +02:00
39fb7f4956 added boost mechanic 2025-10-05 00:13:56 +02:00
75314748da removed sidebar, added names for items 2025-10-04 18:32:27 +02:00
61aa7543be improved sidebar (ordered and colored) 2025-10-04 16:29:03 +02:00
2fac287e1e added sidebar 2025-10-04 16:11:10 +02:00
2a6f2f2a44 added particle effects and sounds 2025-10-04 15:39:19 +02:00
382d850605 added better speed mechanic 2025-10-04 14:03:44 +02:00
a49b3b20cc Merge remote-tracking branch 'origin/develop' into develop-turtleGame 2025-10-04 13:53:17 +02:00
abf907af24 added todos 2025-10-04 13:49:49 +02:00
8bd0ab1974 fixed error after game ends 2025-10-04 13:39:57 +02:00
2c92553a8a added bombs 2025-10-04 13:15:57 +02:00
f26c3a9e6d improved entity collision check while generating 2025-10-04 00:36:50 +02:00
81524cfecf added snacks with collision, switched to multiplayer arena 2025-10-04 00:09:19 +02:00
f2fc4835c3 started random snack generation 2025-10-03 19:32:52 +02:00
abcb23d96a made turtle movement clean 2025-10-03 18:47:11 +02:00
eabbb312b9 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop-turtleGame 2025-10-03 17:29:36 +02:00
d98cebd86f added turtleGame playfield and movement 2025-09-02 22:29:47 +02:00
c87d318421 started turtleGame 2025-09-02 18:22:14 +02:00
5 changed files with 424 additions and 0 deletions

View File

@@ -19,6 +19,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),
@@ -29,6 +30,7 @@ public enum GameList {
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE),
BOWSPLEEF(new BowSpleefFactory(), GameType.PROTOTYPE),
TURTLEGAME(new TurtleGameFactory(), GameType.PROTOTYPE),
TETRIS(new TetrisFactory(), GameType.OTHER),
TNTRUN(new TntRunFactory(), GameType.OTHER),
ANVILRUN(new AnvilRunFactory(), GameType.PVE),

View File

@@ -0,0 +1,263 @@
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<Player, Turtle> turtlePlayerMap = new WeakHashMap<>();
private final ArrayList<Entity> snacks = new ArrayList<>();
private final ArrayList<Entity> 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<Void> callback) {
this.generatePlatform();
}
private void generatePlatform() {
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 = this.snackBlock.registry().material();
if(snackMaterial == null) snackMaterial = Material.DIRT;
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.02f);
}
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<Player> getLeftPlayers() {
return this.turtlePlayerMap.keySet().stream().filter(player -> !player.isFlying()).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.withProperty("half", "upper"));
meta.setCustomName(Component.text("Snack").color(NamedTextColor.WHITE));
meta.setCustomNameVisible(true);
snack.setInstance(this);
Pos spawnPosition = this.newSpawnPosition(snack);
if(spawnPosition == null) {
snack.remove();
return;
}
snack.teleport(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);
bomb.setInstance(this);
Pos spawnPosition = this.newSpawnPosition(bomb, false);
if(spawnPosition == null) {
bomb.remove();
return;
}
bomb.teleport(spawnPosition);
this.bombs.add(bomb);
}
@Nullable
private Pos newSpawnPosition(Entity entity) {
return this.newSpawnPosition(entity, true);
}
@Nullable
private 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((int) Math.ceil(this.turtlePlayerMap.size() * 1.5));
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);
}
}

View File

@@ -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, 4, 6, 8, 10));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
return new TurtleGame(configuration.get("radius").getAsInt(), configuration.get("startSpeed").getAsInt()).setParent(parent);
}
@Override
public Material symbol() {
return Material.TURTLE_EGG;
}
}

View File

@@ -0,0 +1,112 @@
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.teleport(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();
this.kill();
if(this.boostRefillTask.isAlive()) this.boostRefillTask.cancel();
if(this.boostTask.isAlive()) this.boostTask.cancel();
}
public void adaptView() {
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);
System.out.println(this.boostChargeLevel);
this.player.setExp(this.boostChargeLevel);
}, TaskSchedule.millis(30), TaskSchedule.millis(30));
}
public void cancelBoost() {
if(!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);
}
}

View File

@@ -143,3 +143,8 @@ description;Stay on the high ground to win!;Bleibe solange wie möglich auf der
ns:game_Fastbridge#;;
name;Fastbridge;Fastbridge
description;Speedbridge to the other platform. The first one there wins!;Baue dich so schnell wie möglich zur anderen Plattform. Wer zuerst dort ist, gewinnt!
;;
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
Can't render this file because it has a wrong number of fields in line 114.