14 Commits

Author SHA1 Message Date
460d737d3d reset player level 2025-07-19 02:21:23 +02:00
7591ce29ff added strong towers 2025-07-19 02:09:55 +02:00
39bbf19d9d reset more player values in Room 2025-07-19 01:48:35 +02:00
df239d8166 made armadillo faster and chicken slower 2025-07-19 01:27:35 +02:00
bf9c16ccc8 removed log 2025-07-19 01:24:40 +02:00
b5d6d7bac4 added wave mechanic 2025-07-19 01:10:57 +02:00
3b55f16b24 added range to cursor 2025-04-16 23:43:46 +02:00
5ef254b75f fixed tower spawning issues 2025-04-14 17:26:59 +02:00
5e5f36153e added own cursor class 2025-04-13 23:28:46 +02:00
e77879c657 added tower value calculation and delete tower mechanic 2025-04-13 18:35:50 +02:00
e3068be160 moved shooting mechanic to ShootingTower, added money 2025-04-13 16:12:08 +02:00
867bee1c5a corrected PlayerLoginHandler 2025-04-13 13:07:28 +02:00
893923cc56 added towers and added tower spawn mechanics 2025-04-12 23:48:06 +02:00
38c944e6c1 added basic towerdefense logic 2025-04-12 19:22:37 +02:00
47 changed files with 923 additions and 917 deletions

View File

@@ -48,7 +48,7 @@ dependencies {
//Tools
implementation 'de.articdive:jnoise:3.0.2'
implementation 'net.md-5:bungeecord-config:1.21-R0.3'
implementation 'net.md-5:bungeecord-config:1.19-R0.1-SNAPSHOT'
implementation 'org.apache.commons:commons-text:1.10.0'
implementation 'org.spongepowered:configurate-yaml:4.1.2'
implementation 'com.sparkjava:spark-core:2.9.4'

View File

@@ -2,21 +2,17 @@ package eu.mhsl.minenet.minigames.instance.game;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun.AnvilRunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace.BlockBreakRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef.BowSpleefFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.elytraRace.ElytraRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.bedwars.BedwarsFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.acidRain.AcidRainFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube.DeathcubeFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge.FastbridgeFactory;
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.spleef.SpleefFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.stickfight.StickFightFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.TetrisFactory;
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;
@@ -36,11 +32,7 @@ public enum GameList {
ACIDRAIN(new AcidRainFactory(), 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);
JUMPDIVE(new JumpDiveFactory(), GameType.JUMPNRUN);
private final GameFactory factory;
private final GameType type;
@@ -53,6 +45,6 @@ public enum GameList {
}
public GameType getType() {
return this.type;
return type;
}
}

View File

@@ -26,7 +26,7 @@ public class AnvilRunFactory implements GameFactory {
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 5, 10, 15, 20, 25, 30))
.addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 10, 30, 60, 90, 120));
.addOption(new NumericOption("seconds", Material.STICK, TranslatedComponent.byId("optionCommon#seconds"), 10, 30, 60, 90, 120));
}
@Override

View File

@@ -1,79 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.score.FirstWinsScore;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class BlockBreakRace extends StatelessGame {
private int spawnCount = 0;
private final int height;
public BlockBreakRace(int height) {
super(Dimension.OVERWORLD.key, "blockBreakRace", new FirstWinsScore());
this.height = height;
this.setGenerator(new BlockBreakRaceGenerator(height));
}
@Override
protected boolean onPlayerJoin(Player p) {
PlayerInventory inv = p.getInventory();
inv.addItemStack(ItemStack.of(Material.DIAMOND_PICKAXE));
inv.addItemStack(ItemStack.of(Material.DIAMOND_AXE));
inv.addItemStack(ItemStack.of(Material.DIAMOND_SHOVEL));
return super.onPlayerJoin(p);
}
@Override
protected void onStart() {
this.getPlayers().forEach(player -> player.setGameMode(GameMode.SURVIVAL));
}
@Override
protected void onBlockBreak(@NotNull PlayerBlockBreakEvent event) {
List<Material> allowedMaterials = List.of(Material.STONE, Material.OAK_PLANKS, Material.DIRT);
if(!allowedMaterials.contains(event.getBlock().registry().material())) event.setCancelled(true);
if(this.isBeforeBeginning) event.setCancelled(true);
}
@Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
if(this.isBeforeBeginning) return;
if(playerMoveEvent.getNewPosition().y() < BlockBreakRaceGenerator.BOTTOM_Y) {
Player player = playerMoveEvent.getPlayer();
this.getScore().insertResult(player);
player.setGameMode(GameMode.SPECTATOR);
player.getInventory().clear();
}
}
@Override
public Pos getSpawn() {
int idx = this.spawnCount++;
int cols = BlockBreakRaceGenerator.ROW_OFFSETS_X.length;
int rows = BlockBreakRaceGenerator.ROW_OFFSETS_Z.length;
int perChunk = cols * rows;
int zChunk = idx / perChunk;
int gridIndex = idx % perChunk;
int xIndex = gridIndex % cols;
int zIndex = gridIndex / cols;
int localX = BlockBreakRaceGenerator.ROW_OFFSETS_X[xIndex];
int localZ = BlockBreakRaceGenerator.ROW_OFFSETS_Z[zIndex];
int absZ = (zChunk * 16) + localZ;
return new Pos(localX, BlockBreakRaceGenerator.BOTTOM_Y + this.height + 1, absZ).add(0.5);
}
}

View File

@@ -1,40 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace;
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 BlockBreakRaceFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_BlockBreakRace#name");
}
@Override
public Material symbol() {
return Material.DIAMOND_PICKAXE;
}
@Override
public TranslatedComponent description() {
return TranslatedComponent.byId("game_BlockBreakRace#description");
}
@Override
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("height", Material.SCAFFOLDING, TranslatedComponent.byId("optionCommon#height"), 20, 30, 40, 50));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
return new BlockBreakRace(configuration.get("height").getAsInt()).setParent(parent);
}
}

View File

@@ -1,67 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace;
import eu.mhsl.minenet.minigames.world.generator.featureEnriched.ValeGenerator;
import eu.mhsl.minenet.minigames.world.generator.terrain.BaseGenerator;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.GenerationUnit;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public class BlockBreakRaceGenerator extends BaseGenerator {
public static final int BOTTOM_Y = 50;
public final int TOP_Y;
public static final int[] ROW_OFFSETS_X = {4, 8, 12};
public static final int[] ROW_OFFSETS_Z = {4, 8, 12};
private static final Block[] FILL_BLOCKS = {
Block.STONE,
Block.DIRT,
Block.OAK_PLANKS
};
private final Random random = ThreadLocalRandom.current();
public BlockBreakRaceGenerator(int height) {
this.TOP_Y = BOTTOM_Y + height;
ValeGenerator vale = new ValeGenerator();
vale.setXShiftMultiplier(i -> 0.5d);
vale.setHeightNoiseMultiplier(i -> 2);
vale.setXShiftOffset(i -> 40d);
this.addMixIn(vale);
this.addMixIn(unit -> {
if(unit.absoluteStart().chunkX() != 0) return;
for (int localX : ROW_OFFSETS_X) {
for (int localZ : ROW_OFFSETS_Z) {
final int absZ = unit.absoluteStart().blockZ() + localZ;
this.placeTube(unit, localX, absZ);
}
}
});
}
private void placeTube(GenerationUnit unit, int x, int z) {
for (int y = BOTTOM_Y; y < this.TOP_Y; y++) {
Block fill = FILL_BLOCKS[this.random.nextInt(FILL_BLOCKS.length)];
unit.modifier().fill(
new Pos(x, y, z),
new Pos(x, y, z).add(1),
fill
);
}
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if (dx == 0 && dz == 0) continue; // Zentrum überspringen
unit.modifier().fill(
new Pos(x + dx, BOTTOM_Y, z + dz),
new Pos(x + dx, this.TOP_Y + 3, z + dz).add(1),
Block.BARRIER
);
}
}
}
}

View File

@@ -3,8 +3,6 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef;
import eu.mhsl.minenet.minigames.instance.game.Game;
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.restriction.RestrictionHandler;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.common.MinimalPlayeramountGameRestriction;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
import net.minestom.server.item.Material;
@@ -31,10 +29,4 @@ public class BowSpleefFactory implements GameFactory {
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
return new BowSpleef().setParent(parent);
}
@Override
public RestrictionHandler globalRestrictions() {
return new RestrictionHandler()
.addRestriction(new MinimalPlayeramountGameRestriction(2));
}
}

View File

@@ -8,7 +8,6 @@ import eu.mhsl.minenet.minigames.world.BlockPallet;
import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator;
import io.github.togar2.pvp.feature.CombatFeatures;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import org.jetbrains.annotations.NotNull;
@@ -69,9 +68,7 @@ class Deathcube extends StatelessGame {
playerMoveEvent.setCancelled(true);
return;
}
if(playerMoveEvent.getNewPosition().y() <= height) return;
getScore().insertResult(playerMoveEvent.getPlayer());
playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR);
if(playerMoveEvent.getNewPosition().y() > height) getScore().insertResult(playerMoveEvent.getPlayer());
}
@Override

View File

@@ -33,7 +33,6 @@ import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
@@ -184,8 +183,7 @@ public class ElytraRace extends StatelessGame {
}
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(vale.getXShiftAtZ(z), -45, z);
}
private CompletableFuture<Void> generateRing(int zPos) {

View File

@@ -1,64 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.score.FirstWinsScore;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerBlockPlaceEvent;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.instance.Chunk;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import org.jetbrains.annotations.NotNull;
public class Fastbridge extends StatelessGame {
private int currentSpawn = 0;
public Fastbridge() {
super(Dimension.OVERWORLD.key, "Fastbridge", new FirstWinsScore());
this.setGenerator(new FastbridgeChunkgenerator());
}
@Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
Player player = playerMoveEvent.getPlayer();
Pos newPos = playerMoveEvent.getNewPosition();
if(this.getScore().hasResult(player)) return;
if(newPos.y() < 0) {
player.teleport(getSpawn());
if(!isBeforeBeginning) this.resetPlayer(player);
}
if(newPos.x() > 53) {
this.getScore().insertResult(player);
player.setGameMode(GameMode.SPECTATOR);
}
}
@Override
protected void onStart() {
getPlayers().forEach(player -> {
player.setGameMode(GameMode.SURVIVAL);
resetPlayer(player);
});
}
@Override
protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) {
if(isBeforeBeginning) playerBlockPlaceEvent.setCancelled(true);
}
private void resetPlayer(Player player) {
if(isBeforeBeginning) return;
PlayerInventory inventory = player.getInventory();
inventory.clear();
inventory.addItemStack(ItemStack.of(Material.WHITE_WOOL, 64));
}
@Override
public synchronized Pos getSpawn() {
return new Pos(24, 1, currentSpawn++*Chunk.CHUNK_SIZE_Z*2-8, -90, 0);
}
}

View File

@@ -1,29 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge;
import eu.mhsl.minenet.minigames.world.generator.featureEnriched.ValeGenerator;
import eu.mhsl.minenet.minigames.world.generator.terrain.BaseGenerator;
import net.minestom.server.instance.block.Block;
public class FastbridgeChunkgenerator extends BaseGenerator {
public FastbridgeChunkgenerator() {
this.addMixIn(unit -> {
if (unit.absoluteStart().chunkZ() % 2 == 0) {
unit.modifier().fill(Block.BARRIER);
return;
}
if (unit.absoluteStart().chunkX() != 1 && unit.absoluteStart().chunkX() != 3) return;
for (int x = 5; x <= 10; x++){
for (int z = 5; z <= 10; z++){
unit.modifier().setRelative(x, 64, z, unit.absoluteStart().chunkX() == 3 ? Block.GOLD_BLOCK : Block.GRASS_BLOCK);
}
}
});
ValeGenerator vale = new ValeGenerator();
vale.setXShiftMultiplier(integer -> 0.5d);
vale.setHeightNoiseMultiplier(integer -> 2);
vale.setXShiftOffset(integer -> 40d);
this.addMixIn(vale);
}
}

View File

@@ -1,33 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge;
import eu.mhsl.minenet.minigames.instance.game.Game;
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.room.Room;
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
import net.minestom.server.item.Material;
import java.util.Map;
public class FastbridgeFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_Fastbridge#name");
}
@Override
public TranslatedComponent description() {
return TranslatedComponent.byId("game_Fastbridge#description");
}
@Override
public Material symbol() {
return Material.WHITE_WOOL;
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
return new Fastbridge().setParent(parent);
}
}

View File

@@ -1,126 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround;
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.world.BlockPallet;
import io.github.togar2.pvp.events.EntityKnockbackEvent;
import io.github.togar2.pvp.events.FinalAttackEvent;
import io.github.togar2.pvp.events.PrepareAttackEvent;
import io.github.togar2.pvp.feature.CombatFeatures;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player;
import net.minestom.server.event.instance.InstanceTickEvent;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
class HighGround extends StatelessGame {
private final int radius;
private final int seconds;
private final WeakHashMap<Player, Integer> scoreMap = new WeakHashMap<>();
HighGround(int radius, int seconds) {
super(Dimension.THE_END.key, "highground", new PointsWinScore());
this.radius = radius;
this.seconds = seconds;
this.eventNode().addChild(
CombatFeatures.empty()
.add(CombatFeatures.VANILLA_ATTACK)
.add(CombatFeatures.VANILLA_DAMAGE)
.add(CombatFeatures.VANILLA_KNOCKBACK)
.build()
.createNode()
);
this.eventNode().addListener(
FinalAttackEvent.class,
finalAttackEvent -> finalAttackEvent.setBaseDamage(0)
);
this.eventNode().addListener(PrepareAttackEvent.class, prepareAttackEvent -> {
if(this.isBeforeBeginning){
prepareAttackEvent.setCancelled(true);
}
});
this.eventNode().addListener(
EntityKnockbackEvent.class,
entityKnockbackEvent -> entityKnockbackEvent.setStrength(1.1f)
);
this.eventNode().addListener(InstanceTickEvent.class, instanceTickEvent -> {
if (this.isBeforeBeginning || !this.isRunning) return;
this.getPlayers().forEach(player -> {
if((player.isOnGround() && player.getPosition().y() >= 1) || (!player.isOnGround() && player.getPosition().y() >= 1.5)){
this.scoreMap.put(player, this.scoreMap.get(player) + 1);
player.setLevel(this.scoreMap.get(player) / 20);
player.setExp((this.scoreMap.get(player) % 20) / 20.0f);
}
});
});
}
@Override
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
for (int y = 0; y >= -3; y--) {
int radius = (Math.abs(y) * 5) + this.radius;
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
double distance = new Pos(x, 0, z).distance(0, 0, 0);
if (distance <= radius) {
this.setBlock(x, y, z, y == 0 ? Block.DIAMOND_BLOCK : Block.GRASS_BLOCK);
Pos featurePosition = new Pos(x, y + 1, z);
if(y >= 0 || this.getBlock(featurePosition).isSolid()) continue;
if (this.rnd.nextDouble() < 0.1){
this.setBlock(featurePosition, Block.SHORT_GRASS);
}
if (this.rnd.nextDouble() < 0.01){
this.setBlock(featurePosition, BlockPallet.FLOWER.rnd());
}
}
}
}
}
}
@Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
Player player = playerMoveEvent.getPlayer();
if(playerMoveEvent.getNewPosition().y() < -10){
player.teleport(this.getSpawn());
}
}
@Override
protected void start() {
this.getPlayers().forEach(player -> this.scoreMap.put(player, 0));
super.start();
}
@Override
protected void onStart() {
this.setTimeLimit(this.seconds);
}
@Override
protected void onStop() {
this.getPlayers().forEach(player -> this.getScore().insertResult(player, this.scoreMap.get(player)));
}
@Override
public Pos getSpawn() {
double theta = this.rnd.nextDouble() * 2 * Math.PI;
double spawnRadius = this.radius + 5;
double x = spawnRadius * Math.cos(theta);
double z = spawnRadius * Math.sin(theta);
return new Pos(x, 0, z).withLookAt(new Pos(0, 0, 0));
}
}

View File

@@ -1,49 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround;
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.game.stateless.config.restriction.RestrictionHandler;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.common.MinimalPlayeramountGameRestriction;
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 HighGroundFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_Highground#name");
}
@Override
public TranslatedComponent description() {
return TranslatedComponent.byId("game_Highground#description");
}
@Override
public Material symbol() {
return Material.GOLDEN_HELMET;
}
@Override
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 3, 5, 7, 10))
.addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 30, 60, 90, 120));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
return new HighGround(configuration.get("radius").getAsInt(), configuration.get("seconds").getAsInt()).setParent(parent);
}
@Override
public RestrictionHandler globalRestrictions() {
return new RestrictionHandler()
.addRestriction(new MinimalPlayeramountGameRestriction(2));
}
}

View File

@@ -9,7 +9,6 @@ import eu.mhsl.minenet.minigames.world.BlockPallet;
import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.event.player.PlayerStartDiggingEvent;
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
@@ -78,15 +77,6 @@ public class Spleef extends StatelessGame {
}
}
@Override
protected void onBlockBreak(@NotNull PlayerBlockBreakEvent event) {
if(!isRunning) {
event.setCancelled(true);
return;
}
setBlock(event.getBlockPosition(), Block.AIR);
}
private void destroyBlock(PlayerStartDiggingEvent event) {
if(!isRunning) return;
setBlock(event.getBlockPosition(), Block.AIR);

View File

@@ -2,7 +2,7 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.stickfight;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.score.LowestPointsWinScore;
import eu.mhsl.minenet.minigames.score.LastWinsScore;
import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator;
import io.github.togar2.pvp.events.FinalAttackEvent;
import io.github.togar2.pvp.feature.CombatFeatures;
@@ -13,17 +13,15 @@ import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
public class Stickfight extends StatelessGame {
private final double radius = 20;
private final WeakHashMap<Player, Pos> spawnPoints = new WeakHashMap<>();
private final Map<Player, Integer> scoreMap = new WeakHashMap<>();
public Stickfight() {
super(Dimension.OVERWORLD.key, "Stickfight", new LowestPointsWinScore());
super(Dimension.OVERWORLD.key, "Stickfight", new LastWinsScore());
eventNode().addChild(
CombatFeatures.empty()
@@ -68,11 +66,6 @@ public class Stickfight extends StatelessGame {
super.start();
}
@Override
protected void onStop() {
this.scoreMap.forEach((player, score) -> getScore().insertResult(player, score));
}
private void generateBridge(int startX, int startY, int startZ) {
int steps = (int) (radius * 1.5);
for (int i = 0; i < steps; i++) {
@@ -85,22 +78,19 @@ public class Stickfight extends StatelessGame {
@Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
Player player = playerMoveEvent.getPlayer();
if(!spawnPoints.containsKey(player)) {
if(!spawnPoints.containsKey(playerMoveEvent.getPlayer())) {
playerMoveEvent.setCancelled(true);
return;
}
if(isBeforeBeginning) {
if(spawnPoints.get(player).distance(playerMoveEvent.getNewPosition()) < 1) return;
if(spawnPoints.get(playerMoveEvent.getPlayer()).distance(playerMoveEvent.getNewPosition()) < 1) return;
playerMoveEvent.setCancelled(true);
player.teleport(spawnPoints.get(player));
playerMoveEvent.getPlayer().teleport(spawnPoints.get(playerMoveEvent.getPlayer()));
}
if(playerMoveEvent.getNewPosition().y() < 40) {
player.teleport(spawnPoints.get(player));
this.scoreMap.putIfAbsent(player, 0);
this.scoreMap.put(player, this.scoreMap.get(player) + 1);
playerMoveEvent.getPlayer().teleport(spawnPoints.get(playerMoveEvent.getPlayer()));
}
}

View File

@@ -1,129 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.score.LastWinsScore;
import io.github.togar2.pvp.events.FinalAttackEvent;
import io.github.togar2.pvp.events.PrepareAttackEvent;
import io.github.togar2.pvp.feature.CombatFeatures;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.instance.block.Block;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
public class Sumo extends StatelessGame {
int radius;
int health;
int seconds;
int originalRadius;
int timer;
WeakHashMap<Player, Integer> healthMap = new WeakHashMap<>();
public Sumo(int radius, int health, int seconds) {
super(Dimension.OVERWORLD.key, "Sumo", new LastWinsScore());
this.getScore().setIgnoreLastPlayers(1);
this.setTime(6000);
this.setTimeRate(0);
this.radius = radius;
this.health = health;
this.seconds = seconds;
this.originalRadius = radius;
this.timer = seconds;
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 start() {
this.getPlayers().forEach(player -> {
this.healthMap.put(player, this.health);
player.setLevel(this.healthMap.get(player));
});
MinecraftServer.getSchedulerManager().scheduleTask(
() -> {
if(this.isBeforeBeginning) return TaskSchedule.seconds(1);
this.timer--;
double percent = (double) this.timer / this.seconds;
int radius = (int) (this.originalRadius * percent);
if (this.radius >= 5) {
this.radius = radius;
this.generatePlatform();
return TaskSchedule.seconds(1);
}
return TaskSchedule.stop();
},
TaskSchedule.seconds(1)
);
super.start();
}
@Override
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
this.generatePlatform();
}
private void generatePlatform() {
int buffer = 5;
for(int x = -this.radius - buffer; x <= this.radius + buffer; x++) {
for(int z = -this.radius - buffer; z <= this.radius + buffer; 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, isEdge ? Block.RED_CONCRETE : Block.WHITE_CONCRETE);
} else {
this.setBlock(x, 0, z, Block.AIR);
}
}
}
}
@Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
Player player = playerMoveEvent.getPlayer();
if(playerMoveEvent.getNewPosition().y() < -10) {
player.teleport(this.getSpawn());
this.healthMap.put(player, this.healthMap.get(player) - 1);
player.setLevel(this.healthMap.get(player));
if (this.healthMap.get(player) == 0) {
this.getScore().insertResult(player);
player.setGameMode(GameMode.SPECTATOR);
}
}
}
@Override
public Pos getSpawn() {
return new Pos(0, 2, 0);
}
}

View File

@@ -1,49 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo;
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.game.stateless.config.restriction.RestrictionHandler;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.common.MinimalPlayeramountGameRestriction;
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 SumoFactory implements GameFactory {
@Override
public TranslatedComponent name() {
return TranslatedComponent.byId("game_Sumo#name");
}
public TranslatedComponent description() {
return TranslatedComponent.byId("game_Sumo#description");
}
@Override
public Material symbol() {
return Material.SLIME_BALL;
}
@Override
public ConfigManager configuration() {
return new ConfigManager()
.addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30))
.addOption(new NumericOption("health", Material.GOLDEN_APPLE, TranslatedComponent.byId("game_Sumo#lives"), 1, 2, 3, 4, 5))
.addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 30, 60, 90, 120));
}
@Override
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
return new Sumo(configuration.get("radius").getAsInt(), configuration.get("health").getAsInt(), configuration.get("seconds").getAsInt()).setParent(parent);
}
@Override
public RestrictionHandler globalRestrictions() {
return new RestrictionHandler()
.addRestriction(new MinimalPlayeramountGameRestriction(2));
}
}

View File

@@ -28,7 +28,7 @@ class Tetris extends StatelessGame {
public Tetris(int nextTetrominoesCount, boolean isFast, boolean hasCombat) {
super(Dimension.THE_END.key, "Tetris", new PointsWinScore());
this.eventNode()
eventNode()
.addListener(PlayerUseItemEvent.class, this::onPlayerInteract)
.addListener(PlayerHandAnimationEvent.class, this::onPlayerAttack)
.addListener(PlayerTickEvent.class, this::onPlayerTick);
@@ -58,7 +58,7 @@ class Tetris extends StatelessGame {
protected void onStop() {
this.tetrisGames.forEach((player, tetrisGame) -> {
tetrisGame.loose();
this.getScore().insertResult(player, tetrisGame.getScore());
getScore().insertResult(player, tetrisGame.getScore());
tetrisGame.sidebar.removeViewer(player);
});
}
@@ -106,25 +106,23 @@ class Tetris extends StatelessGame {
TetrisGame tetrisGame = this.tetrisGames.get(player);
if(tetrisGame == null) return;
if(tetrisGame.lost && player.getGameMode() != GameMode.SPECTATOR) {
this.letPlayerLoose(player);
letPlayerLoose(player);
}
}
private void letPlayerLoose(Player player) {
TetrisGame tetrisGame = this.tetrisGames.get(player);
if(!this.getScore().hasResult(player)) {
player.setGameMode(GameMode.SPECTATOR);
player.setInvisible(true);
this.getScore().insertResult(player, tetrisGame.getScore());
}
player.setGameMode(GameMode.SPECTATOR);
player.setInvisible(true);
getScore().insertResult(player, tetrisGame.getScore());
boolean allGamesLost = this.tetrisGames.values().stream()
.filter(game -> !game.lost)
.toList()
.isEmpty();
if(!this.setTimeLimit && !allGamesLost) {
if(!setTimeLimit && !allGamesLost) {
this.setTimeLimit(90);
this.setTimeLimit = true;
setTimeLimit = true;
}
}
@@ -136,7 +134,7 @@ class Tetris extends StatelessGame {
if(this.tetrisGames.get(p) == null) {
this.tetrisGames.put(p, new TetrisGame(
this,
this.getSpawn().sub(6, 8, 15).add(this.tetrisGames.size()*30, 0, 0),
getSpawn().sub(6, 8, 15).add(this.tetrisGames.size()*30, 0, 0),
Tetromino.Shape.J,
this.nextTetrominoesCount,
this.isFast,

View File

@@ -74,10 +74,10 @@ public class Playfield {
}
}
batch.setBlock(this.getPlayerSpawnPosition().sub(0, 1, 0), Block.STONE);
batch.setBlock(this.getPlayerSpawnPosition().sub(1, 1, 0), Block.STONE);
batch.setBlock(this.getPlayerSpawnPosition().sub(1, 1, 1), Block.STONE);
batch.setBlock(this.getPlayerSpawnPosition().sub(0, 1, 1), Block.STONE);
batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 0), Block.STONE);
batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 0), Block.STONE);
batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 1), Block.STONE);
batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 1), Block.STONE);
BatchUtil.loadAndApplyBatch(batch, this.instance, () -> {});
}
@@ -90,7 +90,7 @@ public class Playfield {
if(this.instance.getBlock(this.lowerLeftCorner.add(x, y, 1)) == Block.AIR) isFullLine = false;
}
if(isFullLine) {
this.removeFullLine(y);
removeFullLine(y);
removedLinesCounter += 1;
y -= 1;
}
@@ -99,10 +99,10 @@ public class Playfield {
}
public void addLines(int lines) {
int xPosMissing = this.random.nextInt(1, 10);
int xPosMissing = random.nextInt(1, 10);
for (int i = 0; i < lines; i++) {
this.moveAllLinesUp();
moveAllLinesUp();
for (int x = 1; x < 11; x++) {
if(x != xPosMissing) {
this.instance.setBlock(this.lowerLeftCorner.add(x, 1, 1), Block.LIGHT_GRAY_CONCRETE);

View File

@@ -121,8 +121,8 @@ public class TetrisGame {
public void tick() {
if(this.lost || this.paused) return;
if(!this.currentTetromino.moveDown()) {
this.setActiveTetrominoDown();
if(!currentTetromino.moveDown()) {
setActiveTetrominoDown();
}
}
@@ -176,7 +176,7 @@ public class TetrisGame {
}
private boolean switchHold() {
if(!this.holdPossible) return false;
if(!holdPossible) return false;
Tetromino newCurrentTetromino;
if(this.holdTetromino == null) {
@@ -194,7 +194,7 @@ public class TetrisGame {
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
this.currentTetromino.draw();
if(!this.currentTetromino.moveDown()) this.loose();
if(!this.currentTetromino.moveDown()) loose();
double xChange = this.holdTetromino.getXChange();
this.holdTetromino.setPosition(this.holdPosition.add(xChange, 0, 0));
@@ -312,7 +312,7 @@ public class TetrisGame {
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
this.currentTetromino.draw();
if(!this.currentTetromino.moveDown()) {
this.loose();
loose();
}
}
}

View File

@@ -38,13 +38,13 @@ public class Tetromino {
this.uuid = UUID.randomUUID();
switch (this.shape) {
case I -> this.shapeArray = new int[][]{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}};
case J -> this.shapeArray = new int[][]{{1,0,0}, {1,1,1}, {0,0,0}};
case L -> this.shapeArray = new int[][]{{0,0,1}, {1,1,1}, {0,0,0}};
case O -> this.shapeArray = new int[][]{{1,1}, {1,1}};
case S -> this.shapeArray = new int[][]{{0,1,1}, {1,1,0}, {0,0,0}};
case T -> this.shapeArray = new int[][]{{0,1,0}, {1,1,1}, {0,0,0}};
case Z -> this.shapeArray = new int[][]{{1,1,0}, {0,1,1}, {0,0,0}};
case I -> shapeArray = new int[][]{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}};
case J -> shapeArray = new int[][]{{1,0,0}, {1,1,1}, {0,0,0}};
case L -> shapeArray = new int[][]{{0,0,1}, {1,1,1}, {0,0,0}};
case O -> shapeArray = new int[][]{{1,1}, {1,1}};
case S -> shapeArray = new int[][]{{0,1,1}, {1,1,0}, {0,0,0}};
case T -> shapeArray = new int[][]{{0,1,0}, {1,1,1}, {0,0,0}};
case Z -> shapeArray = new int[][]{{1,1,0}, {0,1,1}, {0,0,0}};
}
}
@@ -58,22 +58,22 @@ public class Tetromino {
public boolean rotate(boolean clockwise) {
int[][] newShapeArray = this.getTurnedShapeArray(clockwise);
return this.checkCollisionAndMove(this.position, newShapeArray);
return checkCollisionAndMove(this.position, newShapeArray);
}
public boolean moveDown() {
Pos newPosition = this.position.sub(0, 1, 0);
return this.checkCollisionAndMove(newPosition, this.shapeArray);
return checkCollisionAndMove(newPosition, this.shapeArray);
}
public boolean moveLeft() {
Pos newPosition = this.position.sub(1, 0, 0);
return this.checkCollisionAndMove(newPosition, this.shapeArray);
return checkCollisionAndMove(newPosition, this.shapeArray);
}
public boolean moveRight() {
Pos newPosition = this.position.add(1, 0, 0);
return this.checkCollisionAndMove(newPosition, this.shapeArray);
return checkCollisionAndMove(newPosition, this.shapeArray);
}
public void draw() {
@@ -83,11 +83,11 @@ public class Tetromino {
public void draw(boolean withGhost) {
if(withGhost) {
Pos ghostPos = this.position;
while (!this.checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) {
while (!checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) {
ghostPos = ghostPos.sub(0, 1, 0);
}
Pos positionChange = this.position.sub(ghostPos);
this.getBlockPositions().forEach(pos -> {
getBlockPositions().forEach(pos -> {
Entity ghostBlock = new Entity(ghostEntityType);
((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getGhostBlock());
ghostBlock.setNoGravity(true);
@@ -97,11 +97,11 @@ public class Tetromino {
});
}
this.getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock()));
getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock()));
}
public void drawAsEntities() {
this.getBlockPositions().forEach(pos -> {
getBlockPositions().forEach(pos -> {
Entity ghostBlock = new Entity(ghostEntityType);
((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getColoredBlock());
ghostBlock.setNoGravity(true);
@@ -222,10 +222,10 @@ public class Tetromino {
}
private boolean checkCollision(Pos newPosition, int[][] newShapeArray) {
List<Pos> newBlockPositions = this.getBlockPositions(newPosition, newShapeArray);
List<Pos> newBlockPositions = getBlockPositions(newPosition, newShapeArray);
for(Pos pos : newBlockPositions) {
if(this.isPartOfTetromino(pos)) continue;
if(isPartOfTetromino(pos)) continue;
if(this.instance.getBlock(pos) == this.getGhostBlock()) continue;
if(this.instance.getBlock(pos) != Block.AIR) return true;
}
@@ -234,7 +234,7 @@ public class Tetromino {
}
private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) {
if(!this.checkCollision(newPosition, newShapeArray)) {
if(!checkCollision(newPosition, newShapeArray)) {
this.remove();
this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new);
this.setPosition(newPosition);

View File

@@ -0,0 +1,89 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.Tower;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Material;
import net.minestom.server.network.packet.server.SendablePacket;
import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
public class Cursor extends Entity {
private boolean cursorActive = false;
private final int reach;
public Cursor(int reach) {
super(EntityType.ARMOR_STAND);
this.setBoundingBox(0,0,0);
this.reach = reach;
}
public void updateCursorPosition(Player player) {
Point targetPosition = player.getTargetBlockPosition(this.reach);
this.moveCursorTo(targetPosition);
}
private void moveCursorTo(@Nullable Point newTargetBlockPosition) {
this.setCursorEnabled(true);
if(newTargetBlockPosition == null) {
this.setCursorEnabled(false);
return;
}
if(!this.getInstance().getBlock(newTargetBlockPosition).equals(Block.BLACK_WOOL)) this.setCursorEnabled(false);
Material holdingMaterial = this.getInstance().getPlayerHandMaterial();
if(!this.getInstance().getGame().getAvailableTowers().containsKey(holdingMaterial)) this.setCursorEnabled(false);
this.teleport(new Pos(newTargetBlockPosition.add(0.5,1,0.5)));
this.showRange(this.getInstance().getGame().getAvailableTowers().get(holdingMaterial));
}
private void setCursorEnabled(boolean enabled) {
this.cursorActive = enabled;
this.setInvisible(!enabled);
}
private void showRange(@Nullable Class<? extends Tower> towerClass) {
if(towerClass == null) return;
if(!this.isCursorActive()) return;
try {
int range = towerClass.getConstructor().newInstance().getRange();
Collection<SendablePacket> particles = new ArrayList<>();
double circumference = 2 * Math.PI * range;
int count = (int) (circumference * 1.5);
for (int i = 0; i < count; i++) {
double radians = ((2 * Math.PI) / count) * i;
Vec relativePosition = new Vec(Math.sin(radians)*range, 0, Math.cos(radians)*range);
ParticlePacket particle = new ParticlePacket(Particle.COMPOSTER, this.position.add(relativePosition), Pos.ZERO, 0, 1);
particles.add(particle);
}
this.getInstance().getPlayer().sendPackets(particles);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public TowerdefenseRoom getInstance() {
return (TowerdefenseRoom) super.getInstance();
}
public boolean isCursorActive() {
return this.cursorActive;
}
public Block getTargetBlock() {
return this.getInstance().getBlock(this.getTargetBlockPosition());
}
public Pos getTargetBlockPosition() {
return this.getPosition().sub(0, 0.5, 0);
}
}

View File

@@ -2,39 +2,93 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
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.towerdefense.enemies.WaveGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.*;
import eu.mhsl.minenet.minigames.score.LastWinsScore;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Material;
import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class Towerdefense extends StatelessGame {
private final Random random = new Random();
private final AbsoluteBlockBatch mazeBatch = new AbsoluteBlockBatch();
private final List<Pos> mazePath = new ArrayList<>();
private List<TowerdefenseRoom> instances = new ArrayList<>();
private final List<TowerdefenseRoom> instances = new ArrayList<>();
private final WaveGenerator waveGenerator;
private Task waveTask;
private final Map<Material, Class<? extends Tower>> availableTowers = Map.of(
Material.SKELETON_SPAWN_EGG, SkeletonTower.class,
Material.ZOMBIE_SPAWN_EGG, ZombieTower.class,
Material.BLAZE_SPAWN_EGG, BlazeTower.class,
Material.ENDERMAN_SPAWN_EGG, EndermanTower.class,
Material.PIGLIN_BRUTE_SPAWN_EGG, BruteTower.class,
Material.WITHER_SPAWN_EGG, WitherTower.class,
Material.ENDER_DRAGON_SPAWN_EGG, EnderDragonTower.class
);
private final Map<Class<? extends Tower>, Integer> prices = Map.of(
ZombieTower.class, 4,
SkeletonTower.class, 3,
BlazeTower.class, 8,
EndermanTower.class, 20,
BruteTower.class, 27,
WitherTower.class, 40,
EnderDragonTower.class, 200
);
private static final int pathLength = 10;
public Towerdefense() {
super(Dimension.NETHER.key, "Towerdefense", new LastWinsScore());
setGenerator(new MazeGenerator());
this.setGenerator(new MazeGenerator());
this.generateMaze();
this.waveGenerator = new WaveGenerator(this);
}
@Override
protected void checkAbandoned() {
this.scheduleNextTick((instance) -> {
if(this.instances.stream().allMatch(room -> room.getPlayers().isEmpty()) && this.getPlayers().isEmpty()) {
this.waveTask.cancel();
this.unload();
}
});
}
@Override
protected void onStart() {
this.getPlayers().forEach(player -> {
TowerdefenseRoom newRoom = new TowerdefenseRoom(player, this);
this.instances.add(newRoom);
player.setInstance(newRoom, new Pos(0, 1, 0));
});
this.waveTask = MinecraftServer.getSchedulerManager().scheduleTask(
this.waveGenerator::startNextWave,
TaskSchedule.seconds(3),
TaskSchedule.seconds(25)
);
}
private void generateMaze() {
Pos position = new Pos(0, 0, 0);
this.addMazePosition(position, Block.GREEN_WOOL);
position = position.add(0,0,2);
List<Integer> previousDirections = new ArrayList<>();
int direction = 1; // 0 -> right; 1 -> straight; 2 -> left
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 3; j++) {
for (int i = 0; i < pathLength; i++) {
for (int j = 0; j < 9; j++) {
position = position.add(direction-1,0,direction%2);
this.addMazePosition(position, Block.WHITE_WOOL);
}
@@ -48,12 +102,16 @@ public class Towerdefense extends StatelessGame {
previousDirections.add(direction);
}
this.addMazePosition(position, Block.WHITE_WOOL);
this.addMazePosition(position.add(0,0,1), Block.WHITE_WOOL);
this.addMazePosition(position.add(0,0,2), Block.RED_WOOL);
this.addMazePosition(position.add(0,0,3), Block.WHITE_WOOL);
this.addMazePosition(position.add(0,0,6), Block.RED_WOOL);
}
private void addMazePosition(Pos position, Block pathBlock) {
this.mazeBatch.setBlock(position, pathBlock);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
this.mazeBatch.setBlock(position.add(i-1,0,j-1), pathBlock);
}
}
this.mazePath.add(position.add(0.5,1,0.5));
}
@@ -65,12 +123,21 @@ public class Towerdefense extends StatelessGame {
return this.mazePath;
}
public List<TowerdefenseRoom> getInstances() {
return this.instances;
}
@Override
protected boolean onPlayerJoin(Player p) {
TowerdefenseRoom newRoom = new TowerdefenseRoom(p, this);
this.instances.add(newRoom);
p.setInstance(newRoom);
newRoom.startWave(List.of(EntityType.ENDERMAN, EntityType.BLAZE, EntityType.PLAYER, EntityType.HORSE, EntityType.ARMOR_STAND, EntityType.SKELETON));
return false;
MinecraftServer.getSchedulerManager().scheduleNextTick(() -> p.teleport(new Pos((long) -this.getPlayers().size(), 1, 0)));
return super.onPlayerJoin(p);
}
public Map<Class<? extends Tower>, Integer> getPrices() {
return this.prices;
}
public Map<Material, Class<? extends Tower>> getAvailableTowers() {
return this.availableTowers;
}
}

View File

@@ -1,22 +1,47 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies.GroupFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.generator.MazeGenerator;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers.Tower;
import eu.mhsl.minenet.minigames.util.BatchUtil;
import eu.mhsl.minenet.minigames.util.CommonEventHandles;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.minestom.server.MinecraftServer;
import net.minestom.server.collision.Aerodynamics;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.*;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.event.entity.EntityDeathEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import net.minestom.server.event.inventory.InventoryPreClickEvent;
import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.item.PlayerBeginItemUseEvent;
import net.minestom.server.event.player.*;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.ItemAnimation;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class TowerdefenseRoom extends InstanceContainer {
private final static int reach = 30;
private long lastPlayerAttack = 0;
private final Player player;
private final Towerdefense game;
private final List<EntityCreature> enemies = new ArrayList<>();
private final List<Tower> towers = new ArrayList<>();
private final Cursor cursor;
private int money = 0;
public TowerdefenseRoom(Player player, Towerdefense game) {
super(UUID.randomUUID(), Dimension.OVERWORLD.key);
@@ -25,34 +50,189 @@ public class TowerdefenseRoom extends InstanceContainer {
this.game = game;
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setAllowFlying(true);
this.player.getInventory().setItemStack(0, ItemStack.of(Material.ARMOR_STAND));
this.addMoney(0);
this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(reach);
this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(reach);
setGenerator(new MazeGenerator());
this.player.getInventory().addItemStack(
ItemStack.of(Material.BOW).withCustomName(Component.text("Schießen", TextColor.color(255, 180, 0)))
);
this.player.getInventory().addItemStack(
ItemStack.of(Material.BARRIER).withCustomName(Component.text("Löschen", TextColor.color(255,0,0)))
);
this.game.getAvailableTowers().forEach((material, tower) -> {
int price = this.game.getPrices().get(tower);
this.player.getInventory().addItemStack(ItemStack.of(material).withMaxStackSize(price).withAmount(price));
});
this.setGenerator(new MazeGenerator());
BatchUtil.loadAndApplyBatch(this.game.getMazeBatch(), this, () -> {});
this.cursor = new Cursor(reach);
this.cursor.setInstance(this);
this.cursor.setBoundingBox(0,0,0);
this.eventNode()
.addListener(EntityDeathEvent.class, event -> {
if(!(event.getEntity() instanceof EntityCreature enemy)) return;
this.enemies.remove(enemy);
})
.addListener(PlayerTickEvent.class, event -> this.cursor.updateCursorPosition(this.player))
.addListener(PlayerUseItemEvent.class, event -> this.useItem())
.addListener(PlayerUseItemOnBlockEvent.class, event -> this.useItem())
.addListener(PlayerEntityInteractEvent.class, event -> this.useItem())
.addListener(PlayerHandAnimationEvent.class, event -> this.useItem())
.addListener(ItemDropEvent.class, CommonEventHandles::cancel)
.addListener(InventoryPreClickEvent.class, CommonEventHandles::cancel)
.addListener(PlayerSwapItemEvent.class, CommonEventHandles::cancel)
.addListener(PlayerBeginItemUseEvent.class, event -> {
if(event.getAnimation().equals(ItemAnimation.BOW)) event.setCancelled(true);
});
}
public void startWave(List<EntityType> entities) {
int counter = 0;
for(EntityType entityType : entities) {
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
this.addEntity(new EntityCreature(entityType));
return TaskSchedule.stop();
}, TaskSchedule.millis(800L*counter));
counter++;
}
}
private void addEntity(EntityCreature entity) {
entity.setInstance(this, this.game.getMazePath().getFirst());
entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.15);
entity.getNavigator().setPathTo(this.game.getMazePath().get(1), 0.7, () -> changeEntityGoal(entity, 1));
}
private void changeEntityGoal(EntityCreature entity, int positionIndex) {
if(positionIndex == this.game.getMazePath().size()-1) {
private void useItem() {
Material itemInHand = this.player.getItemInMainHand().material();
if(itemInHand.equals(Material.BOW)) {
this.playerAttack();
return;
}
entity.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.7, () -> changeEntityGoal(entity, positionIndex+1));
if(itemInHand.equals(Material.BARRIER)) {
this.removeTower();
return;
}
this.placeTower();
}
private void playerAttack() {
if(this.lastPlayerAttack > System.currentTimeMillis()-200) return;
Player p = this.player;
this.lastPlayerAttack = System.currentTimeMillis();
EntityProjectile projectile = new EntityProjectile(p, EntityType.SPECTRAL_ARROW);
projectile.setView(p.getPosition().yaw(), p.getPosition().pitch());
projectile.setNoGravity(true);
projectile.setAerodynamics(new Aerodynamics(0, 1, 1));
Vec projectileVelocity = p.getPosition().direction().normalize().mul(20);
projectile.setVelocity(projectileVelocity);
projectile.scheduleRemove(Duration.ofSeconds(5));
projectile.setInstance(this, p.getPosition().add(0, p.getEyeHeight()-0.5, 0));
projectile.eventNode()
.addListener(ProjectileCollideWithEntityEvent.class, hitEvent -> {
if(!(hitEvent.getTarget() instanceof EntityCreature target)) return;
if(!this.getEnemies().stream().filter(entityCreature -> !entityCreature.isDead()).toList().contains(target)) return;
target.damage(DamageType.PLAYER_ATTACK, 0.1f);
if(target.isDead()) this.addMoney((int) target.getAttribute(Attribute.MAX_HEALTH).getBaseValue());
projectile.remove();
})
.addListener(ProjectileCollideWithBlockEvent.class, collisionEvent -> projectile.remove());
}
public void addMoney(int amount) {
this.money += amount;
this.player.setLevel(this.money);
}
public Material getPlayerHandMaterial() {
return this.player.getItemInMainHand().material();
}
private synchronized void placeTower() {
if(!this.canPlaceActiveTower()) {
if(this.cursor.isCursorActive() && !this.enoughMoneyForActiveTower()) {
this.player.sendActionBar(Component.text("Nicht genug Geld!", TextColor.color(255,0,0)));
}
return;
}
try {
Material usedMaterial = this.player.getItemInMainHand().material();
Tower tower = this.game.getAvailableTowers().entrySet().stream()
.filter(materialClassEntry -> materialClassEntry.getKey().equals(usedMaterial))
.findFirst()
.orElseThrow()
.getValue()
.getConstructor()
.newInstance();
this.setBlock(this.cursor.getTargetBlockPosition(), Block.BLUE_WOOL);
tower.setInstance(this, this.cursor.getPosition());
this.towers.add(tower);
tower.startShooting();
this.addMoney(-this.game.getPrices().get(tower.getClass()));
} catch (Exception ignored) {
}
this.cursor.updateCursorPosition(this.player);
}
private void removeTower() {
Entity entity = this.player.getLineOfSightEntity(reach, entity1 -> true);
if(!(entity instanceof Tower tower)) return;
if(!this.towers.contains(tower)) return;
this.setBlock(tower.getPosition().sub(0, 0.5, 0), Block.BLACK_WOOL);
this.addMoney(tower.getSellingPrice(this.game.getPrices().get(tower.getClass())));
this.towers.remove(tower);
tower.remove();
}
private boolean canPlaceActiveTower() {
Material usedMaterial = this.player.getItemInMainHand().material();
return this.canPlaceTower(this.game.getAvailableTowers().get(usedMaterial));
}
private boolean canPlaceTower(@Nullable Class<? extends Tower> towerClass) {
if(towerClass == null) return false;
return this.cursor.isCursorActive() && this.enoughMoney(towerClass);
}
private boolean enoughMoneyForActiveTower() {
Material usedMaterial = this.player.getItemInMainHand().material();
return this.enoughMoney(this.game.getAvailableTowers().get(usedMaterial));
}
private boolean enoughMoney(@Nullable Class<? extends Tower> towerClass) {
if(towerClass == null) return true;
return this.money >= this.game.getPrices().get(towerClass);
}
void startWave(List<GroupFactory> enemyGroups) {
enemyGroups.forEach(groupFactory -> groupFactory.summonGroup(this));
}
public void addEnemy(EntityCreature enemy) {
enemy.setInstance(this, this.game.getMazePath().getFirst());
this.enemies.add(enemy);
this.changeEnemyGoal(enemy, 0);
}
private synchronized void changeEnemyGoal(EntityCreature enemy, int positionIndex) {
if(positionIndex == this.game.getMazePath().size()-1) {
this.enemies.remove(enemy);
enemy.remove();
float damage = (float) Math.ceil(enemy.getHealth()/10);
if(this.player.getHealth() - damage <= 0) {
this.getEnemies().forEach(Entity::remove);
this.towers.forEach(Entity::remove);
this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(this.player.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).attribute().defaultValue());
this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(this.player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).attribute().defaultValue());
this.player.setInstance(this.game).thenRun(() -> MinecraftServer.getInstanceManager().unregisterInstance(this));
this.player.heal();
this.game.getScore().insertResult(this.player);
return;
}
this.player.damage(DamageType.PLAYER_ATTACK, damage);
return;
}
enemy.getNavigator().setPathTo(this.game.getMazePath().get(positionIndex+1), 0.6, () -> this.changeEnemyGoal(enemy, positionIndex+1));
}
public Towerdefense getGame() {
return this.game;
}
public List<EntityCreature> getEnemies() {
return this.enemies;
}
public Player getPlayer() {
return this.player;
}
}

View File

@@ -0,0 +1,33 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.attribute.Attribute;
public record EnemyFactory(EntityType entityType, float health, double speed) {
/**
* Factory for a tower defense enemy.
* @param entityType type of enemy
* @param health base health (between 0 and 1024, default 10)
* @param speed walk speed (default 0.1)
*/
public EnemyFactory {
if(health > 1024 || health <= 0) throw new IllegalArgumentException("Enemy health has to be between 0 and 1024");
}
public EnemyFactory(EntityType entityType) {
this(entityType, 10, 0.1);
}
public EntityCreature buildEntity() {
EntityCreature entity = new EntityCreature(this.entityType);
entity.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(this.speed);
entity.getAttribute(Attribute.MAX_HEALTH).setBaseValue(this.health);
entity.setHealth(this.health);
return entity;
}
public int getStrength() {
return (int) Math.floor(this.health * this.speed * 5);
}
}

View File

@@ -0,0 +1,16 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseRoom;
import net.minestom.server.MinecraftServer;
import net.minestom.server.timer.TaskSchedule;
public record GroupFactory(EnemyFactory enemyFactory, int count, long delay) {
public void summonGroup(TowerdefenseRoom instance) {
for (int i = 0; i < this.count; i++) {
MinecraftServer.getSchedulerManager().scheduleTask(() -> {
instance.addEnemy(this.enemyFactory.buildEntity());
return TaskSchedule.stop();
}, TaskSchedule.millis(this.delay*i));
}
}
}

View File

@@ -0,0 +1,77 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.enemies;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.Towerdefense;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.EntityType;
import net.minestom.server.timer.TaskSchedule;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
public class WaveGenerator {
final Towerdefense game;
int currentLevel;
Random random = new Random();
private final Map<EnemyFactory, Integer> availableEnemies = Map.of(
new EnemyFactory(EntityType.PIG, 2, 0.1), new EnemyFactory(EntityType.PIG, 2, 0.1).getStrength(),
new EnemyFactory(EntityType.VILLAGER, 6, 0.1), new EnemyFactory(EntityType.VILLAGER, 6, 0.1).getStrength(),
new EnemyFactory(EntityType.SHEEP, 4, 0.2), new EnemyFactory(EntityType.SHEEP, 4, 0.2).getStrength(),
new EnemyFactory(EntityType.COW, 10, 0.1), new EnemyFactory(EntityType.COW, 10, 0.1).getStrength(),
new EnemyFactory(EntityType.CAMEL, 10, 0.2), new EnemyFactory(EntityType.CAMEL, 10, 0.2).getStrength(),
new EnemyFactory(EntityType.ARMADILLO, 20, 0.4), new EnemyFactory(EntityType.ARMADILLO, 20, 0.3).getStrength(),
new EnemyFactory(EntityType.CHICKEN, 8, 0.5), new EnemyFactory(EntityType.CHICKEN, 8, 0.6).getStrength()
);
public WaveGenerator(Towerdefense game) {
this(game, 1);
}
public WaveGenerator(Towerdefense game, int startLevel) {
this.game = game;
this.currentLevel = startLevel - 1;
}
public void startNextWave() {
this.currentLevel += 1;
List<EnemyFactory> enemyList = this.chooseEnemies();
Collections.shuffle(enemyList);
int averageDelay = Math.min(20000 / enemyList.size(), 1500);
AtomicInteger delay = new AtomicInteger(0);
enemyList.forEach(enemy -> {
delay.addAndGet(averageDelay);
MinecraftServer.getSchedulerManager().scheduleTask(
() -> this.spawnEnemy(enemyList.removeFirst()),
TaskSchedule.millis(delay.get() + this.random.nextInt(averageDelay-200, averageDelay+200)),
TaskSchedule.stop()
);
});
}
private void spawnEnemy(EnemyFactory enemy) {
this.game.getInstances().forEach(instance -> instance.addEnemy(enemy.buildEntity()));
}
private int getCurrentLevelStrength() {
return (int) (0.5 * Math.pow(2, this.currentLevel));
}
private List<EnemyFactory> chooseEnemies() {
int strength = this.getCurrentLevelStrength();
final List<EnemyFactory> usedEnemies = new ArrayList<>();
while(strength > 0) {
int finalStrength = strength;
Map.Entry<EnemyFactory, Integer> chosenEnemy = this.availableEnemies.entrySet().stream()
.filter(e -> e.getValue() <= finalStrength)
.sorted((o1, o2) -> ThreadLocalRandom.current().nextInt(-1, 2))
.findAny()
.get();
usedEnemies.add(chosenEnemy.getKey());
strength -= chosenEnemy.getKey().getStrength();
}
return usedEnemies;
}
}

View File

@@ -0,0 +1,16 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
public class BlazeTower extends ShootingTower {
public BlazeTower() {
super(EntityType.BLAZE, 2, 2, 15);
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.shootProjectile(enemy, EntityType.FIREBALL, 2, 0);
}
}

View File

@@ -0,0 +1,19 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public class BruteTower extends Tower {
public BruteTower() {
super(EntityType.PIGLIN_BRUTE, 20, 2, 4);
this.setItemInMainHand(ItemStack.of(Material.GOLDEN_AXE));
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.causeDamage(enemy);
}
}

View File

@@ -0,0 +1,20 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
public class EnderDragonTower extends ShootingTower{
public EnderDragonTower() {
super(EntityType.ENDER_DRAGON, 40, 7, 20);
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.DRAGON_FIREBALL, 2, 0);
}
@Override
protected void onProjectileHit(EntityCreature enemy) {
super.onProjectileHit(enemy);
}
}

View File

@@ -0,0 +1,16 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
public class EndermanTower extends Tower {
public EndermanTower() {
super(EntityType.ENDERMAN, 12, 1, 6);
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.causeDamage(enemy);
}
}

View File

@@ -0,0 +1,63 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.collision.Aerodynamics;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityProjectile;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent;
import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
public abstract class ShootingTower extends Tower {
public ShootingTower(@NotNull EntityType entityType, int damage, double attacksPerSecond, int range) {
super(entityType, damage, attacksPerSecond, range);
}
protected void shootProjectile(EntityCreature enemy, EntityType projectileType, double power, double spread) {
EntityProjectile projectile = new EntityProjectile(this, projectileType);
projectile.setView(this.getPosition().yaw(), this.getPosition().pitch());
projectile.setNoGravity(true);
projectile.setAerodynamics(new Aerodynamics(0, 1, 1));
Pos startingPoint = this.getPosition().add(0, this.getEyeHeight(), 0);
double enemySpeed = enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue() / 0.05;
Point enemyGoal = enemy.getNavigator().getGoalPosition();
if(enemyGoal == null) enemyGoal = enemy.getPosition();
Pos enemyPosition = enemy.getPosition();
Vec enemyMovement = Vec.fromPoint(enemyGoal.sub(enemyPosition)).normalize().mul(enemySpeed);
// just an approximation, not calculated correctly:
double projectileSpeed = 20 * power;
double enemyTowerDistance = startingPoint.distance(enemyPosition);
double estimatedFlightTime = (enemyTowerDistance / projectileSpeed);
Pos targetPosition = enemyPosition.add(enemyMovement.mul(estimatedFlightTime)).withY(enemyPosition.y()+enemy.getEyeHeight());
projectile.shoot(targetPosition, power, spread);
projectile.scheduleRemove(Duration.ofSeconds(5));
projectile.setInstance(this.getInstance(), startingPoint);
projectile.eventNode()
.addListener(ProjectileCollideWithEntityEvent.class, event -> {
if(!(event.getTarget() instanceof EntityCreature target)) return;
if(!this.getRoomInstance().getEnemies().contains(target)) return;
this.causeDamage(target);
this.onProjectileHit(target);
target.setFlyingWithElytra(true);
projectile.remove();
})
.addListener(ProjectileCollideWithBlockEvent.class, event -> projectile.remove());
}
protected void onProjectileHit(EntityCreature enemy) {
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
}
}

View File

@@ -0,0 +1,25 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public class SkeletonTower extends ShootingTower {
public SkeletonTower() {
super(EntityType.SKELETON, 1, 2, 10);
this.setItemInMainHand(ItemStack.of(Material.BOW));
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.shootProjectile(enemy, EntityType.ARROW, 2, 0);
}
@Override
protected void onProjectileHit(EntityCreature enemy) {
super.onProjectileHit(enemy);
// enemy.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(enemy.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue()*0.5);
}
}

View File

@@ -0,0 +1,135 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseRoom;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.*;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Stream;
public abstract class Tower extends EntityCreature {
public enum Priority {
FIRST(Type.PATH_DISTANCE),
LAST(Type.PATH_DISTANCE),
STRONG(Type.HEALTH),
WEAK(Type.HEALTH),
CLOSE(Type.TOWER_DISTANCE),
FAR(Type.TOWER_DISTANCE);
final Type type;
Priority(Type type) {
this.type = type;
}
public Type getType() {
return this.type;
}
public enum Type {
PATH_DISTANCE,
HEALTH,
TOWER_DISTANCE
}
}
private final static int damageDivider = 10;
private Priority priority = Priority.FIRST;
protected float damage;
protected int range;
protected double attacksPerSecond;
protected TaskSchedule attackDelay;
private @Nullable Task attackTask;
private float sellingPriceMultiplier = 1;
public Tower(@NotNull EntityType entityType, int damage, double attacksPerSecond, int range) {
super(entityType);
this.damage = (float) damage / damageDivider;
this.range = range;
this.attacksPerSecond = attacksPerSecond;
this.attackDelay = TaskSchedule.millis((long) (1000/attacksPerSecond));
}
public void startShooting() {
this.attackTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> {
EntityCreature nextEnemy = this.getNextEnemy();
if(nextEnemy == null) return this.attackDelay;
this.lookAt(nextEnemy);
this.attack(nextEnemy);
return this.attackDelay;
}, TaskSchedule.immediate());
}
@Override
protected void remove(boolean permanent) {
if(this.attackTask != null) this.attackTask.cancel();
super.remove(permanent);
}
private @Nullable EntityCreature getNextEnemy() {
List<EntityCreature> enemies = this.getSortedEnemies();
if(enemies.isEmpty()) return null;
return switch (this.priority) {
case LAST, STRONG, FAR -> enemies.getLast();
case FIRST, WEAK, CLOSE -> enemies.getFirst();
};
}
private List<EntityCreature> getSortedEnemies() {
Stream<EntityCreature> enemyStream = this.getRoomInstance().getEnemies().stream().parallel()
.filter(enemy -> !enemy.isDead())
.filter(enemy -> enemy.getPosition().distance(this.getPosition()) <= this.range);
return switch (this.priority.getType()) {
case PATH_DISTANCE -> enemyStream
.sorted((o1, o2) -> {
Pos endPoint = this.getRoomInstance().getGame().getMazePath().getLast();
return Double.compare(
o1.getPosition().distance(endPoint),
o2.getPosition().distance(endPoint)
);
}).toList();
case TOWER_DISTANCE -> enemyStream
.sorted((o1, o2) -> Double.compare(
o1.getPosition().distance(this.getPosition()),
o2.getPosition().distance(this.getPosition())
)).toList();
case HEALTH -> enemyStream
.sorted((o1, o2) -> Float.compare(o1.getHealth(), o2.getHealth()))
.toList();
};
}
protected TowerdefenseRoom getRoomInstance() {
return (TowerdefenseRoom) this.getInstance();
}
public int getSellingPrice(int buyPrice) {
return (int) (this.sellingPriceMultiplier * buyPrice);
}
public double getValue() {
return this.damage * this.attacksPerSecond * this.range * 2;
}
public int getRange() {
return this.range;
}
protected void causeDamage(EntityCreature enemy) {
this.causeDamage(enemy, this.damage);
}
protected void causeDamage(EntityCreature enemy, float amount) {
enemy.damage(DamageType.PLAYER_ATTACK, amount);
if(enemy.isDead()) this.getRoomInstance().addMoney((int) enemy.getAttribute(Attribute.MAX_HEALTH).getBaseValue());
}
protected abstract void attack(EntityCreature enemy);
}

View File

@@ -0,0 +1,20 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
public class WitherTower extends ShootingTower {
public WitherTower() {
super(EntityType.WITHER, 18, 4, 13);
}
@Override
protected void attack(EntityCreature enemy) {
this.shootProjectile(enemy, EntityType.WITHER_SKULL, 2, 0);
}
@Override
protected void onProjectileHit(EntityCreature enemy) {
super.onProjectileHit(enemy);
}
}

View File

@@ -0,0 +1,19 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.towers;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
public class ZombieTower extends Tower {
public ZombieTower() {
super(EntityType.ZOMBIE, 7, 1, 4);
this.setItemInMainHand(ItemStack.of(Material.IRON_SWORD));
}
@Override
protected void attack(EntityCreature enemy) {
this.swingMainHand();
this.causeDamage(enemy);
}
}

View File

@@ -156,7 +156,6 @@ class TrafficLightRace extends StatelessGame {
@Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
if(isBeforeBeginning) return;
if(getScore().hasResult(playerMoveEvent.getPlayer())) return;
if(phase.equals(LightPhase.RED) && playerMoveEvent.getNewPosition().z()-0.01 > playerMoveEvent.getPlayer().getPosition().z()) {
playerMoveEvent.getPlayer().setVelocity(new Vec(0, 8, -15));

View File

@@ -15,6 +15,7 @@ import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import net.minestom.server.instance.anvil.AnvilLoader;
@@ -66,6 +67,9 @@ public class Room extends MineNetInstance implements Spawnable {
p.getInventory().clear();
p.setGameMode(GameMode.ADVENTURE);
p.setInvisible(false);
p.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(p.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).attribute().defaultValue());
p.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(p.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).attribute().defaultValue());
p.heal();
p.setExp(0);
p.setLevel(0);
rooms.add(room);
@@ -90,29 +94,29 @@ public class Room extends MineNetInstance implements Spawnable {
private Room(Player owner) {
super(Dimension.THE_END.key);
this.apiDriven = false;
this.construct();
this.setOwner(owner);
construct();
setOwner(owner);
}
protected Room() {
super(Dimension.THE_END.key);
this.apiDriven = true;
this.construct();
construct();
}
private void construct() {
MinecraftServer.getInstanceManager().registerInstance(this);
this.setChunkLoader(new AnvilLoader(Resource.LOBBY_MAP.getPath()));
setChunkLoader(new AnvilLoader(Resource.LOBBY_MAP.getPath()));
this.gameSelector = new GameSelector();
this.gameSelector.setInstance(this, new Pos(0.5, 50, 19.5));
this.eventNode().addListener(PlayerBlockBreakEvent.class, CommonEventHandles::cancel);
this.eventNode().addListener(PlayerDisconnectEvent.class, playerDisconnectEvent -> unsetRoom(playerDisconnectEvent.getPlayer()));
eventNode().addListener(PlayerBlockBreakEvent.class, CommonEventHandles::cancel);
eventNode().addListener(PlayerDisconnectEvent.class, playerDisconnectEvent -> unsetRoom(playerDisconnectEvent.getPlayer()));
}
public Player getOwner() {
return this.owner;
return owner;
}
public void setOwner(Player newOwner) {
@@ -125,7 +129,7 @@ public class Room extends MineNetInstance implements Spawnable {
if(p != this.owner) return;
this.getAllMembers().stream()
getAllMembers().stream()
.filter(player -> player != p) // exclude the current leaving owner
.findFirst()
.ifPresentOrElse(
@@ -135,8 +139,8 @@ 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.ERROR).appendStatic("The room leader left!").send(getAllMembers());
new ChatMessage(Icon.SCIENCE).appendStatic(this.owner.getUsername()).appendStatic(" is the new Leader!").send(getAllMembers().stream().filter(player -> player != this.owner).collect(Collectors.toSet()));
new ChatMessage(Icon.SUCCESS).appendStatic("You are now the leader.").send(this.owner);
});
}

View File

@@ -1,29 +0,0 @@
package eu.mhsl.minenet.minigames.score;
import eu.mhsl.minenet.minigames.util.MapUtil;
import net.minestom.server.entity.Player;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class LowestPointsWinScore extends PointsWinScore {
@Override
protected void insertResultImplementation(Set<Player> p, int currentPoints) {
Set<Player> combined = scoreOrder.entrySet().stream()
.filter(entrySet -> Objects.equals(entrySet.getValue(), currentPoints))
.map(Map.Entry::getKey)
.findFirst()
.orElseGet(() -> {
Set<Player> s = new HashSet<>();
scoreOrder.put(s, currentPoints);
return s;
});
combined.addAll(p);
this.scoreOrder = MapUtil.sortReversedByValue(this.scoreOrder);
getScores().clear();
this.scoreOrder.forEach((player, integer) -> getScores().addFirst(player));
}
}

View File

@@ -12,21 +12,11 @@ import java.util.*;
import java.util.stream.Collectors;
public class PointsWinScore extends Score {
Map<Set<Player>, Integer> scoreOrder = new HashMap<>();
private Map<Set<Player>, Integer> scoreOrder = new HashMap<>();
@Override
protected void insertResultImplementation(Set<Player> p, int currentPoints) {
Set<Player> combined = scoreOrder.entrySet().stream()
.filter(entrySet -> Objects.equals(entrySet.getValue(), currentPoints))
.map(Map.Entry::getKey)
.findFirst()
.orElseGet(() -> {
Set<Player> s = new HashSet<>();
scoreOrder.put(s, currentPoints);
return s;
});
combined.addAll(p);
this.scoreOrder.put(p, currentPoints);
this.scoreOrder = MapUtil.sortByValue(this.scoreOrder);
getScores().clear();
this.scoreOrder.forEach((player, integer) -> getScores().addFirst(player));
@@ -52,12 +42,13 @@ public class PointsWinScore extends Score {
.filter(player -> !player.getUsername().isBlank())
.toList()
.isEmpty())
.map(players -> players.stream()
.map(Player::getUsername)
.sorted()
.collect(Collectors.joining(", "))
+ " : " + scoreOrder.get(players)
).toList()
.map(players -> players
.stream()
.filter(player -> scoreOrder.get(Set.of(player)) != null)
.map(player -> player.getUsername()+" : "+scoreOrder.get(Set.of(player)).toString())
.collect(Collectors.joining(", "))
)
.toList()
)
.undent()
.newLine()

View File

@@ -4,7 +4,6 @@ import eu.mhsl.minenet.minigames.score.Score;
import net.minestom.server.entity.Player;
import java.util.*;
import java.util.stream.Collectors;
public class Tournament {
private final List<Score> gameScores = new ArrayList<>();
@@ -44,11 +43,9 @@ public class Tournament {
public Rewards getRewards() {
Map<UUID, Integer> itemCount = new HashMap<>();
int count = 0;
for (Set<Player> players : getPlaces()) {
for (Player player : getPlaces()) {
if(count >= this.rewardConfiguration.rewardCount().size()) break;
for(Player player : players) {
itemCount.put(player.getUuid(), this.rewardConfiguration.rewardCount().get(count));
}
itemCount.put(player.getUuid(), this.rewardConfiguration.rewardCount().get(count));
count++;
}
@@ -56,21 +53,18 @@ public class Tournament {
this.memorialConfiguration.memorialMaterial().namespace().value(),
this.memorialConfiguration.memorialTitle(),
this.memorialConfiguration.memorialLore(),
getGameScores().keySet().stream().map(Player::getUuid).toList(),
getPlaces().stream().map(Player::getUuid).toList(),
this.rewardConfiguration.item().namespace().value(),
itemCount
);
}
public List<Set<Player>> getPlaces() {
List<Set<Player>> players = new ArrayList<>(
public List<Player> getPlaces() {
List<Player> players = new ArrayList<>(
getGameScores().entrySet().stream()
.collect(
Collectors.groupingBy(
Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toSet())
)
).values()
.sorted(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.toList()
);
Collections.reverse(players);

View File

@@ -12,12 +12,10 @@ import net.minestom.server.entity.*;
import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.instance.anvil.AnvilLoader;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
public class TournamentDisplay extends MineNetInstance implements Spawnable {
private final List<Set<Player>> places;
private final List<Player> places;
private final Tournament tournament;
private final Pos[] placePositions = new Pos[] {
@@ -32,7 +30,7 @@ public class TournamentDisplay extends MineNetInstance implements Spawnable {
this.places = tournament.getPlaces();
this.tournament = tournament;
this.places.forEach(players -> players.forEach(player -> player.setGameMode(GameMode.ADVENTURE)));
this.places.forEach(player -> player.setGameMode(GameMode.ADVENTURE));
eventNode().addListener(PlayerMoveEvent.class, playerMoveEvent -> {
if(isOnDisplay(playerMoveEvent.getPlayer()) && !playerMoveEvent.getNewPosition().sameBlock(placePositions[getRankPosition(playerMoveEvent.getPlayer())])) {
@@ -47,10 +45,7 @@ public class TournamentDisplay extends MineNetInstance implements Spawnable {
}
private int getRankPosition(Player player) {
for (int i = 0; i < places.size(); i++) {
if (places.get(i).contains(player)) return i;
}
return -1;
return this.places.indexOf(player);
}
@Override
@@ -61,11 +56,8 @@ public class TournamentDisplay extends MineNetInstance implements Spawnable {
p.teleport(placePositions[getRankPosition(p)]);
}
});
List<Player> players = this.places.stream()
.flatMap(s -> s.stream().sorted(Comparator.comparing(Player::getUsername)))
.toList();
new ChatMessage(Icon.STAR)
.list(players.stream().map(player -> String.format("%d. %s - %s Punkte", this.getRankPosition(player)+1, player.getUsername(), tournament.getGameScores().get(player))).toList())
.numberedList(this.places.stream().map(player -> String.format("%s - %s Punkte", player.getUsername(), tournament.getGameScores().get(player))).toList())
.send(p);
return false;
}

View File

@@ -1,6 +1,9 @@
package eu.mhsl.minenet.minigames.util;
import java.util.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class MapUtil {
public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
@@ -14,16 +17,4 @@ public class MapUtil {
return result;
}
public static <K, V extends Comparable<? super V>> Map<K, V> sortReversedByValue(Map<K, V> map) {
List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
list.sort(Map.Entry.<K, V>comparingByValue().reversed());
Map<K, V> result = new LinkedHashMap<>();
for (Map.Entry<K, V> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
}

View File

@@ -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}),
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<Block> list;

View File

@@ -21,7 +21,6 @@ public class ValeGenerator extends HeightTerrainGenerator {
.build();
private Function<Integer, Double> xShiftMultiplier = multiplier -> 1d;
private Function<Integer, Double> xShiftOffset = z -> 0d;
public ValeGenerator() {
setCalculateHeight(this::calculateY);
@@ -33,14 +32,10 @@ public class ValeGenerator extends HeightTerrainGenerator {
}
public int getXShiftAtZ(int z) {
return (int) ((curves.getNoise(z) * 32 + largeCurves.getNoise(z) * 64) * xShiftMultiplier.apply(z) + xShiftOffset.apply(z));
return (int) ((curves.getNoise(z) * 32 + largeCurves.getNoise(z) * 64) * xShiftMultiplier.apply(z));
}
public void setXShiftMultiplier(Function<Integer, Double> xShiftMultiplier) {
this.xShiftMultiplier = xShiftMultiplier;
}
public void setXShiftOffset(Function<Integer, Double> xShiftOffset) {
this.xShiftOffset = xShiftOffset;
}
}

View File

@@ -130,21 +130,3 @@ description;Run away from falling anvils;Renne von fallenden Ambossen davon
ns:game_jumpDive#;;
name;Jump dive;Wassersprung
description;Jump into the water, avoiding already used spots!;Springe ins wasser an stellen, in denen noch niemand zuvor gelandet ist!
;;
ns:game_Sumo#;;
name;Sumo;Sumo
lives;Lives;Leben
description;Knock your enemies off and stay on top!;Versuche deinen Gegner von der Plattform zu schubsen!
;;
ns:game_Highground#;;
name;Highground;Hochburg
description;Stay on the high ground to win!;Bleibe solange wie möglich auf der Hochburg, um zu gewinnen!
;;
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_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!
;;
Can't render this file because it has a wrong number of fields in line 114.