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.tntrun.TntRunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory; 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.trafficlightrace.TrafficLightRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame.TurtleGameFactory;
public enum GameList { public enum GameList {
DEATHCUBE(new DeathcubeFactory(), GameType.JUMPNRUN), DEATHCUBE(new DeathcubeFactory(), GameType.JUMPNRUN),
@@ -29,6 +30,7 @@ public enum GameList {
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE), BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE), BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE),
BOWSPLEEF(new BowSpleefFactory(), GameType.PROTOTYPE), BOWSPLEEF(new BowSpleefFactory(), GameType.PROTOTYPE),
TURTLEGAME(new TurtleGameFactory(), GameType.PROTOTYPE),
TETRIS(new TetrisFactory(), GameType.OTHER), TETRIS(new TetrisFactory(), GameType.OTHER),
TNTRUN(new TntRunFactory(), GameType.OTHER), TNTRUN(new TntRunFactory(), GameType.OTHER),
ANVILRUN(new AnvilRunFactory(), GameType.PVE), 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#;; ns:game_Fastbridge#;;
name;Fastbridge;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! 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.