Merge branch 'refs/heads/develop-tetris' into develop
# Conflicts: # src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java # src/main/resources/lang/locales.map.csv
This commit is contained in:
commit
0a1ae69f53
@ -11,6 +11,7 @@ import eu.mhsl.minenet.minigames.instance.Spawnable;
|
|||||||
import eu.mhsl.minenet.minigames.instance.room.Room;
|
import eu.mhsl.minenet.minigames.instance.room.Room;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.coordinate.Pos;
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.entity.GameMode;
|
||||||
import net.minestom.server.entity.Player;
|
import net.minestom.server.entity.Player;
|
||||||
import net.minestom.server.event.item.ItemDropEvent;
|
import net.minestom.server.event.item.ItemDropEvent;
|
||||||
import net.minestom.server.event.player.PlayerBlockBreakEvent;
|
import net.minestom.server.event.player.PlayerBlockBreakEvent;
|
||||||
@ -98,7 +99,11 @@ public abstract class Game extends MineNetInstance implements Spawnable {
|
|||||||
public void unload() {
|
public void unload() {
|
||||||
this.onUnload();
|
this.onUnload();
|
||||||
|
|
||||||
getPlayers().forEach(Room::setOwnRoom);
|
getPlayers().forEach(player -> {
|
||||||
|
Room.setOwnRoom(player);
|
||||||
|
player.setGameMode(GameMode.SURVIVAL);
|
||||||
|
player.setInvisible(false);
|
||||||
|
});
|
||||||
|
|
||||||
scheduler().scheduleTask(() -> {
|
scheduler().scheduleTask(() -> {
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube.Deathcu
|
|||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory;
|
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.spleef.SpleefFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.stickfight.StickFightFactory;
|
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.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;
|
||||||
@ -23,9 +24,10 @@ public enum GameList {
|
|||||||
TOWERDEFENSE(new TowerdefenseFactory(), GameType.PROTOTYPE),
|
TOWERDEFENSE(new TowerdefenseFactory(), GameType.PROTOTYPE),
|
||||||
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
|
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
|
||||||
BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE),
|
BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE),
|
||||||
|
ANVILRUN(new AnvilRunFactory(), GameType.PROTOTYPE),
|
||||||
|
TETRIS(new TetrisFactory(), GameType.OTHER),
|
||||||
TNTRUN(new TntRunFactory(), GameType.OTHER),
|
TNTRUN(new TntRunFactory(), GameType.OTHER),
|
||||||
ACIDRAIN(new AcidRainFactory(), GameType.PVE),
|
ACIDRAIN(new AcidRainFactory(), GameType.PVE),
|
||||||
ANVILRUN(new AnvilRunFactory(), GameType.PVE),
|
|
||||||
ELYTRARACE(new ElytraRaceFactory(), GameType.PVP),
|
ELYTRARACE(new ElytraRaceFactory(), GameType.PVP),
|
||||||
SPLEEF(new SpleefFactory(), GameType.PVP),
|
SPLEEF(new SpleefFactory(), GameType.PVP),
|
||||||
BOWSPLEEF(new BowSpleefFactory(), GameType.PVP);
|
BOWSPLEEF(new BowSpleefFactory(), GameType.PVP);
|
||||||
|
@ -50,8 +50,8 @@ public class StatelessGame extends Game {
|
|||||||
|
|
||||||
int timeLeft = timeLimit - timePlayed;
|
int timeLeft = timeLimit - timePlayed;
|
||||||
switch (timeLeft) {
|
switch (timeLeft) {
|
||||||
case 60, 30, 10, 5, 4, 3, 2, 1 ->
|
case 90, 60, 30, 10, 5, 4, 3, 2, 1 ->
|
||||||
new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers());
|
new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers());
|
||||||
}
|
}
|
||||||
|
|
||||||
timePlayed++;
|
timePlayed++;
|
||||||
|
@ -9,6 +9,7 @@ import net.minestom.server.item.ItemStack;
|
|||||||
import net.minestom.server.item.Material;
|
import net.minestom.server.item.Material;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public abstract class Option<T> {
|
public abstract class Option<T> {
|
||||||
private RestrictionHandler restrictionHandler;
|
private RestrictionHandler restrictionHandler;
|
||||||
@ -25,7 +26,7 @@ public abstract class Option<T> {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
currentValue = options.get(0);
|
currentValue = options.getFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRestrictionHandler(RestrictionHandler restrictionHandler) {
|
public void setRestrictionHandler(RestrictionHandler restrictionHandler) {
|
||||||
@ -44,11 +45,11 @@ public abstract class Option<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ItemStack getCurrent(Player p) {
|
public ItemStack getCurrent(Player p) {
|
||||||
int amount = Integer.parseInt(options.get(pointer).toString());
|
String value = options.get(pointer).toString();
|
||||||
return ItemStack.builder(item)
|
return ItemStack.builder(item)
|
||||||
.customName(name.getAssembled(p))
|
.customName(name.getAssembled(p))
|
||||||
.lore(TranslatedComponent.byId("optionCommon#value").setColor(NamedTextColor.GOLD).getAssembled(p)
|
.lore(TranslatedComponent.byId("optionCommon#value").setColor(NamedTextColor.GOLD).getAssembled(p)
|
||||||
.append(Component.text(": ")).append(Component.text(amount)))
|
.append(Component.text(": ")).append(Component.text(value)))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +57,10 @@ public abstract class Option<T> {
|
|||||||
return Integer.parseInt(getAsString());
|
return Integer.parseInt(getAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getAsBoolean() {
|
||||||
|
return Objects.equals(getAsString(), "true") || Objects.equals(getAsString(), "1");
|
||||||
|
}
|
||||||
|
|
||||||
public String getAsString() {
|
public String getAsString() {
|
||||||
return currentValue.toString();
|
return currentValue.toString();
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ import net.minestom.server.item.Material;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class BoolOption extends Option<Boolean> {
|
public class BoolOption extends Option<String> {
|
||||||
public BoolOption(String id, Material item, TranslatedComponent name) {
|
public BoolOption(String id, Material item, TranslatedComponent name) {
|
||||||
super(id, item, name, List.of(true, false));
|
super(id, item, name, List.of("true", "false"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ public class ElytraRace extends StatelessGame {
|
|||||||
private final ValeGenerator vale = new ValeGenerator();
|
private final ValeGenerator vale = new ValeGenerator();
|
||||||
|
|
||||||
private final int gameHeight = 0;
|
private final int gameHeight = 0;
|
||||||
|
private final int seaLevel = -55;
|
||||||
private final int ringSpacing = 100;
|
private final int ringSpacing = 100;
|
||||||
private final int ringCount;
|
private final int ringCount;
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ public class ElytraRace extends StatelessGame {
|
|||||||
this.ringCount = ringCount;
|
this.ringCount = ringCount;
|
||||||
|
|
||||||
setGenerator(vale);
|
setGenerator(vale);
|
||||||
vale.setCalculateSeaLevel(point -> -55);
|
vale.setCalculateSeaLevel(point -> seaLevel);
|
||||||
vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1));
|
vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1));
|
||||||
vale.addMixIn(new PlaneTerrainGenerator(gameHeight, Block.BARRIER));
|
vale.addMixIn(new PlaneTerrainGenerator(gameHeight, Block.BARRIER));
|
||||||
|
|
||||||
@ -106,9 +107,7 @@ public class ElytraRace extends StatelessGame {
|
|||||||
@Override
|
@Override
|
||||||
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
|
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
|
||||||
Point spawnpoint = new Pos(vale.getXShiftAtZ(0), -46, 0);
|
Point spawnpoint = new Pos(vale.getXShiftAtZ(0), -46, 0);
|
||||||
GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> {
|
GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> setBlock(point, BlockPallet.STREET.rnd()));
|
||||||
setBlock(point, BlockPallet.STREET.rnd());
|
|
||||||
});
|
|
||||||
|
|
||||||
generateRing(ringSpacing);
|
generateRing(ringSpacing);
|
||||||
generateRing(ringSpacing * 2);
|
generateRing(ringSpacing * 2);
|
||||||
@ -195,8 +194,8 @@ public class ElytraRace extends StatelessGame {
|
|||||||
|
|
||||||
Point ringPos = getRingPositionAtZ(zPos);
|
Point ringPos = getRingPositionAtZ(zPos);
|
||||||
GeneratorUtils.iterateArea(
|
GeneratorUtils.iterateArea(
|
||||||
ringPos.sub(100, 0, 0).withY(0), // TODO 0 was before update getDimensionType().getMinY, might not work correctly
|
ringPos.sub(100, 0, 0).withY(0),
|
||||||
ringPos.add(100, 0, 0).withY(gameHeight),
|
ringPos.add(100, 0, 0).withY(seaLevel),
|
||||||
point -> batch.setBlock(point, Block.BARRIER)
|
point -> batch.setBlock(point, Block.BARRIER)
|
||||||
);
|
);
|
||||||
GeneratorUtils.iterateArea(
|
GeneratorUtils.iterateArea(
|
||||||
|
@ -0,0 +1,185 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.TetrisGame;
|
||||||
|
import eu.mhsl.minenet.minigames.instance.Dimension;
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.Tetromino;
|
||||||
|
import eu.mhsl.minenet.minigames.score.PointsWinScore;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.coordinate.Vec;
|
||||||
|
import net.minestom.server.entity.Entity;
|
||||||
|
import net.minestom.server.entity.GameMode;
|
||||||
|
import net.minestom.server.entity.Player;
|
||||||
|
import net.minestom.server.event.player.*;
|
||||||
|
import net.minestom.server.item.ItemStack;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
class Tetris extends StatelessGame {
|
||||||
|
private final Map<Player, TetrisGame> tetrisGames = new WeakHashMap<>();
|
||||||
|
private final int nextTetrominoesCount;
|
||||||
|
private final boolean isFast;
|
||||||
|
private final boolean hasCombat;
|
||||||
|
private boolean setTimeLimit = false;
|
||||||
|
private final long randomSeed;
|
||||||
|
|
||||||
|
public Tetris(int nextTetrominoesCount, boolean isFast, boolean hasCombat) {
|
||||||
|
super(Dimension.THE_END.key, "Tetris", new PointsWinScore());
|
||||||
|
|
||||||
|
eventNode()
|
||||||
|
.addListener(PlayerUseItemEvent.class, this::onPlayerInteract)
|
||||||
|
.addListener(PlayerHandAnimationEvent.class, this::onPlayerAttack)
|
||||||
|
.addListener(PlayerTickEvent.class, this::onPlayerTick);
|
||||||
|
|
||||||
|
this.nextTetrominoesCount = nextTetrominoesCount;
|
||||||
|
this.isFast = isFast;
|
||||||
|
this.hasCombat = hasCombat;
|
||||||
|
|
||||||
|
Random random = new Random();
|
||||||
|
this.randomSeed = random.nextLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
this.getEntities().stream()
|
||||||
|
.filter(entity -> entity.getEntityType().equals(Tetromino.getGhostEntityType()))
|
||||||
|
.forEach(Entity::remove);
|
||||||
|
|
||||||
|
if(this.hasCombat) {
|
||||||
|
this.tetrisGames.values().forEach(tetrisGame -> tetrisGame.updateOtherTetrisGames(this.tetrisGames.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tetrisGames.forEach((player, tetrisGame) -> tetrisGame.start());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
this.tetrisGames.forEach((player, tetrisGame) -> {
|
||||||
|
tetrisGame.loose();
|
||||||
|
getScore().insertResult(player, tetrisGame.getScore());
|
||||||
|
tetrisGame.sidebar.removeViewer(player);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPlayerLeave(Player p) {
|
||||||
|
this.tetrisGames.get(p).sidebar.removeViewer(p);
|
||||||
|
this.letPlayerLoose(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPlayerMove(@NotNull PlayerMoveEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
Pos previousPosition = event.getPlayer().getPosition();
|
||||||
|
Pos currentPosition = event.getNewPosition();
|
||||||
|
|
||||||
|
TetrisGame tetrisGame = this.tetrisGames.get(player);
|
||||||
|
|
||||||
|
if(tetrisGame == null) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(tetrisGame.lost) return;
|
||||||
|
if(player.getGameMode() == GameMode.SPECTATOR) return;
|
||||||
|
|
||||||
|
event.setNewPosition(tetrisGame.getPlayerSpawnPosition().withView(currentPosition));
|
||||||
|
player.setSprinting(false);
|
||||||
|
|
||||||
|
Vec movementVector = currentPosition.asVec().sub(previousPosition.asVec());
|
||||||
|
|
||||||
|
float yaw = player.getPosition().yaw();
|
||||||
|
|
||||||
|
double yawRadians = Math.toRadians(yaw);
|
||||||
|
double forwardX = -Math.sin(yawRadians);
|
||||||
|
double forwardZ = Math.cos(yawRadians);
|
||||||
|
|
||||||
|
Vec forward = new Vec(forwardX, 0, forwardZ).normalize();
|
||||||
|
Vec left = forward.cross(new Vec(0, 1, 0)).normalize();
|
||||||
|
|
||||||
|
double forwardAmount = movementVector.dot(forward);
|
||||||
|
double leftAmount = movementVector.dot(left);
|
||||||
|
|
||||||
|
double buttonPressAmount = 0.018;
|
||||||
|
if (forwardAmount > buttonPressAmount) {
|
||||||
|
tetrisGame.pressedButton(TetrisGame.Button.W);
|
||||||
|
} else if (forwardAmount < -buttonPressAmount) {
|
||||||
|
tetrisGame.pressedButton(TetrisGame.Button.S);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftAmount > buttonPressAmount) {
|
||||||
|
tetrisGame.pressedButton(TetrisGame.Button.D);
|
||||||
|
} else if (leftAmount < -buttonPressAmount) {
|
||||||
|
tetrisGame.pressedButton(TetrisGame.Button.A);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(previousPosition.y() < currentPosition.y()) tetrisGame.pressedButton(TetrisGame.Button.space);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onPlayerInteract(@NotNull PlayerUseItemEvent event) {
|
||||||
|
this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onPlayerAttack(@NotNull PlayerHandAnimationEvent event) {
|
||||||
|
this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onPlayerTick(PlayerTickEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
TetrisGame tetrisGame = this.tetrisGames.get(player);
|
||||||
|
if(tetrisGame == null) return;
|
||||||
|
if(tetrisGame.lost && player.getGameMode() != GameMode.SPECTATOR) {
|
||||||
|
letPlayerLoose(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void letPlayerLoose(Player player) {
|
||||||
|
TetrisGame tetrisGame = this.tetrisGames.get(player);
|
||||||
|
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(!setTimeLimit && !allGamesLost) {
|
||||||
|
this.setTimeLimit(90);
|
||||||
|
setTimeLimit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onPlayerJoin(Player p) {
|
||||||
|
p.getInventory().setItemStack(0, ItemStack.builder(Material.BIRCH_BUTTON).customName(Component.text("Controller")).build());
|
||||||
|
p.setSprinting(false);
|
||||||
|
|
||||||
|
if(this.tetrisGames.get(p) == null) {
|
||||||
|
this.tetrisGames.put(p, new TetrisGame(
|
||||||
|
this,
|
||||||
|
getSpawn().sub(6, 8, 15).add(this.tetrisGames.size()*30, 0, 0),
|
||||||
|
Tetromino.Shape.J,
|
||||||
|
this.nextTetrominoesCount,
|
||||||
|
this.isFast,
|
||||||
|
this.hasCombat,
|
||||||
|
this.randomSeed
|
||||||
|
));
|
||||||
|
this.tetrisGames.get(p).generate();
|
||||||
|
this.tetrisGames.values().forEach(tetrisGame -> tetrisGame.updateOtherTetrisGames(this.tetrisGames.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
TetrisGame tetrisGame = this.tetrisGames.get(p);
|
||||||
|
|
||||||
|
p.teleport(tetrisGame.getPlayerSpawnPosition());
|
||||||
|
tetrisGame.sidebar.addViewer(p);
|
||||||
|
|
||||||
|
return super.onPlayerJoin(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pos getSpawn() {
|
||||||
|
return new Pos(0, 30, 15).withView(180, 0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris;
|
||||||
|
|
||||||
|
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.ConfigManager;
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.BoolOption;
|
||||||
|
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 TetrisFactory implements GameFactory {
|
||||||
|
@Override
|
||||||
|
public TranslatedComponent name() {
|
||||||
|
return TranslatedComponent.byId("game_Tetris#name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TranslatedComponent description() {
|
||||||
|
return TranslatedComponent.byId("game_Tetris#description");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConfigManager configuration() {
|
||||||
|
return new ConfigManager()
|
||||||
|
.addOption(new NumericOption("nextTetrominoesCount", Material.LADDER, TranslatedComponent.byId("game_Tetris#nextTetrominoesCount"), 3, 4, 5, 1, 2))
|
||||||
|
.addOption(new BoolOption("isFast", Material.MINECART, TranslatedComponent.byId("game_Tetris#isFast")))
|
||||||
|
.addOption(new BoolOption("hasCombat", Material.DIAMOND_SWORD, TranslatedComponent.byId("game_Tetris#hasCombat")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Game manufacture(Room parent, Map<String, Option<?>> configuration) {
|
||||||
|
return new Tetris(configuration.get("nextTetrominoesCount").getAsInt(), configuration.get("isFast").getAsBoolean(), configuration.get("hasCombat").getAsBoolean()).setParent(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Material symbol() {
|
||||||
|
return Material.PURPLE_WOOL;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||||
|
import eu.mhsl.minenet.minigames.util.BatchUtil;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class Playfield {
|
||||||
|
private final Pos lowerLeftCorner;
|
||||||
|
private final StatelessGame instance;
|
||||||
|
private final static int height = 22;
|
||||||
|
private final static Block scoreBlock = Block.STONE;
|
||||||
|
private final int nextTetrominoesCount;
|
||||||
|
private final Random random;
|
||||||
|
|
||||||
|
public Playfield(Pos lowerLeftCorner, StatelessGame instance, int nextTetrominoesCount) {
|
||||||
|
this.nextTetrominoesCount = nextTetrominoesCount;
|
||||||
|
this.lowerLeftCorner = lowerLeftCorner;
|
||||||
|
this.instance = instance;
|
||||||
|
this.random = new Random();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pos getPlayerSpawnPosition() {
|
||||||
|
return this.lowerLeftCorner.add(6, 9, 20).withView(180, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pos getTetrominoSpawnPosition() {
|
||||||
|
return this.lowerLeftCorner.add(5, 21, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pos getHoldPosition() {
|
||||||
|
return this.lowerLeftCorner.add(-4, 18, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pos getNextPosition() {
|
||||||
|
return this.lowerLeftCorner.add(15, 18, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pos getScorePosition() {
|
||||||
|
return this.lowerLeftCorner.add(-5, height+3, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generate() {
|
||||||
|
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
|
||||||
|
|
||||||
|
// actual playfield:
|
||||||
|
for(int x=0; x<12; x++) {
|
||||||
|
for(int y = 0; y< height; y++) {
|
||||||
|
batch.setBlock(this.lowerLeftCorner.add(x, y, 0), Block.GLASS);
|
||||||
|
batch.setBlock(this.lowerLeftCorner.add(x, y, -1), Block.BLACK_CONCRETE);
|
||||||
|
|
||||||
|
if(x==0 || x==11 || y==0) {
|
||||||
|
batch.setBlock(this.lowerLeftCorner.add(x, y, 1), Block.GRAY_CONCRETE);
|
||||||
|
batch.setBlock(this.lowerLeftCorner.add(x, y, 0), Block.GRAY_CONCRETE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hold position:
|
||||||
|
for(int x = 0; x < 4; x++) {
|
||||||
|
for(int y = 0; y < 4; y++) {
|
||||||
|
batch.setBlock(this.getHoldPosition().add(x-1, y-1, -1), Block.QUARTZ_BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// next positions:
|
||||||
|
for(int x = 0; x < 4; x++) {
|
||||||
|
for(int y = -4*this.nextTetrominoesCount+4; y < 4; y++) {
|
||||||
|
batch.setBlock(this.getNextPosition().add(x-1, y-1, -1), Block.QUARTZ_BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, () -> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeFullLines() {
|
||||||
|
int removedLinesCounter = 0;
|
||||||
|
for(int y = 1; y< height; y++) {
|
||||||
|
boolean isFullLine = true;
|
||||||
|
for(int x=1; x<11; x++) {
|
||||||
|
if(this.instance.getBlock(this.lowerLeftCorner.add(x, y, 1)) == Block.AIR) isFullLine = false;
|
||||||
|
}
|
||||||
|
if(isFullLine) {
|
||||||
|
removeFullLine(y);
|
||||||
|
removedLinesCounter += 1;
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removedLinesCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addLines(int lines) {
|
||||||
|
int xPosMissing = random.nextInt(1, 10);
|
||||||
|
|
||||||
|
for (int i = 0; i < lines; i++) {
|
||||||
|
moveAllLinesUp();
|
||||||
|
for (int x = 1; x < 11; x++) {
|
||||||
|
if(x != xPosMissing) {
|
||||||
|
this.instance.setBlock(this.lowerLeftCorner.add(x, 1, 1), Block.LIGHT_GRAY_CONCRETE);
|
||||||
|
} else {
|
||||||
|
this.instance.setBlock(this.lowerLeftCorner.add(x, 1, 1), Block.AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAttackingLines(int attackingLines) {
|
||||||
|
for (int y = 0; y < height + 5; y++) {
|
||||||
|
if(attackingLines > 0) {
|
||||||
|
this.instance.setBlock(this.lowerLeftCorner.add(12, y, 1), Block.REDSTONE_BLOCK);
|
||||||
|
attackingLines -= 1;
|
||||||
|
} else {
|
||||||
|
this.instance.setBlock(this.lowerLeftCorner.add(12, y, 1), Block.AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateScore(int score) {
|
||||||
|
this.removeDigits();
|
||||||
|
String scoreString = String.valueOf(score);
|
||||||
|
char[] characters = scoreString.toCharArray();
|
||||||
|
ArrayUtils.reverse(characters);
|
||||||
|
for(int i = 6; i > 0; i--) {
|
||||||
|
char digit;
|
||||||
|
if(i <= characters.length) {
|
||||||
|
digit = characters[i-1];
|
||||||
|
} else {
|
||||||
|
digit = '0';
|
||||||
|
}
|
||||||
|
this.displayDigit(digit, 6-i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void displayDigit(char digit, int positionFromLeft) {
|
||||||
|
int[][] digitArray;
|
||||||
|
switch (digit) {
|
||||||
|
case '1' -> digitArray = new int[][]{{0,0,1},{0,1,1},{0,0,1},{0,0,1},{0,0,1}};
|
||||||
|
case '2' -> digitArray = new int[][]{{1,1,1},{0,0,1},{1,1,1},{1,0,0},{1,1,1}};
|
||||||
|
case '3' -> digitArray = new int[][]{{1,1,1},{0,0,1},{0,1,1},{0,0,1},{1,1,1}};
|
||||||
|
case '4' -> digitArray = new int[][]{{1,0,1},{1,0,1},{1,1,1},{0,0,1},{0,0,1}};
|
||||||
|
case '5' -> digitArray = new int[][]{{1,1,1},{1,0,0},{1,1,1},{0,0,1},{1,1,1}};
|
||||||
|
case '6' -> digitArray = new int[][]{{1,1,1},{1,0,0},{1,1,1},{1,0,1},{1,1,1}};
|
||||||
|
case '7' -> digitArray = new int[][]{{1,1,1},{0,0,1},{0,1,0},{0,1,0},{0,1,0}};
|
||||||
|
case '8' -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,1,1},{1,0,1},{1,1,1}};
|
||||||
|
case '9' -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,1,1},{0,0,1},{1,1,1}};
|
||||||
|
default -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,0,1},{1,0,1},{1,1,1}};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int x = 0; x < 3; x++) {
|
||||||
|
for (int y = 0; y < 5; y++) {
|
||||||
|
if(digitArray[4-y][x] == 1) this.instance.setBlock(this.getScorePosition().add(positionFromLeft*4+x, y, 0), scoreBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeDigits() {
|
||||||
|
for (int x = 0; x < 4 * 6; x++) {
|
||||||
|
for (int y = 0; y < 5; y++) {
|
||||||
|
this.instance.setBlock(this.getScorePosition().add(x, y, 0), Block.AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveAllLinesUp() {
|
||||||
|
for (int y = height + 3; y > 1; y--) {
|
||||||
|
for (int x = 1; x < 11; x++) {
|
||||||
|
Block blockBeneath = this.instance.getBlock(this.lowerLeftCorner.add(x, y - 1, 1));
|
||||||
|
this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), blockBeneath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeFullLine(int positionY) {
|
||||||
|
for(int y = positionY; y< height; y++) {
|
||||||
|
for(int x=1; x<11; x++) {
|
||||||
|
Block blockAbove = this.instance.getBlock(this.lowerLeftCorner.add(x, y+1, 1));
|
||||||
|
this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), blockAbove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,318 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.scoreboard.Sidebar;
|
||||||
|
import net.minestom.server.timer.Scheduler;
|
||||||
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class TetrisGame {
|
||||||
|
private final StatelessGame instance;
|
||||||
|
private final Playfield playfield;
|
||||||
|
private final boolean isFast;
|
||||||
|
private int level = 1;
|
||||||
|
private int lines = 0;
|
||||||
|
private int score = 0;
|
||||||
|
private int combo = 0;
|
||||||
|
private int attackingLines = 0;
|
||||||
|
public boolean lost = false;
|
||||||
|
public boolean paused = true;
|
||||||
|
private final boolean hasCombat;
|
||||||
|
public Tetromino currentTetromino;
|
||||||
|
private final List<Tetromino> nextTetrominoes = new ArrayList<>();
|
||||||
|
private Tetromino holdTetromino;
|
||||||
|
private final List<Tetromino> tetrominoBag = new ArrayList<>();
|
||||||
|
private boolean holdPossible = true;
|
||||||
|
private final Pos nextPosition;
|
||||||
|
private final Pos holdPosition;
|
||||||
|
private final Pos tetrominoSpawnPosition;
|
||||||
|
public Sidebar sidebar = new Sidebar(Component.text("Info:"));
|
||||||
|
private final Map<Button, Long> lastPresses = new HashMap<>();
|
||||||
|
private final List<TetrisGame> otherTetrisGames = new ArrayList<>();
|
||||||
|
private final Random random;
|
||||||
|
|
||||||
|
public enum Button {
|
||||||
|
W,
|
||||||
|
A,
|
||||||
|
S,
|
||||||
|
D,
|
||||||
|
mouseLeft,
|
||||||
|
mouseRight,
|
||||||
|
space
|
||||||
|
}
|
||||||
|
|
||||||
|
public TetrisGame(StatelessGame instance, Pos lowerLeftCorner, Tetromino.Shape startTetrominoShape, int nextTetrominoesCount, boolean isfast, boolean hasCombat, long randomSeed) {
|
||||||
|
this.isFast = isfast;
|
||||||
|
this.hasCombat = hasCombat;
|
||||||
|
this.instance = instance;
|
||||||
|
this.playfield = new Playfield(lowerLeftCorner, this.instance, nextTetrominoesCount);
|
||||||
|
this.random = new Random(randomSeed);
|
||||||
|
|
||||||
|
this.holdPosition = this.playfield.getHoldPosition();
|
||||||
|
this.nextPosition = this.playfield.getNextPosition();
|
||||||
|
this.tetrominoSpawnPosition = this.playfield.getTetrominoSpawnPosition();
|
||||||
|
this.buildSidebar();
|
||||||
|
|
||||||
|
this.currentTetromino = new Tetromino(this.instance, startTetrominoShape);
|
||||||
|
for (int i = 0; i < nextTetrominoesCount; i++) {
|
||||||
|
this.updateNextTetrominoes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pressedButton(Button button) {
|
||||||
|
final int standardButtonDelay = 100;
|
||||||
|
final int buttonDebounce = 70;
|
||||||
|
|
||||||
|
if(this.lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis()-standardButtonDelay) return;
|
||||||
|
|
||||||
|
this.lastPresses.put(button, System.currentTimeMillis());
|
||||||
|
if(button == Button.W) this.lastPresses.put(button, System.currentTimeMillis()+buttonDebounce);
|
||||||
|
if(button == Button.S) this.lastPresses.put(button, System.currentTimeMillis()-buttonDebounce);
|
||||||
|
|
||||||
|
if(this.lost || this.paused) return;
|
||||||
|
|
||||||
|
switch (button) {
|
||||||
|
case A -> this.currentTetromino.moveLeft();
|
||||||
|
case S -> this.moveDown();
|
||||||
|
case D -> this.currentTetromino.moveRight();
|
||||||
|
case W -> this.hardDrop();
|
||||||
|
case mouseLeft -> this.currentTetromino.rotate(false);
|
||||||
|
case mouseRight -> this.currentTetromino.rotate(true);
|
||||||
|
case space -> this.switchHold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pos getPlayerSpawnPosition() {
|
||||||
|
return this.playfield.getPlayerSpawnPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
this.paused = false;
|
||||||
|
Scheduler scheduler = MinecraftServer.getSchedulerManager();
|
||||||
|
scheduler.submitTask(() -> {
|
||||||
|
if(this.lost) return TaskSchedule.stop();
|
||||||
|
int standardTickDelay = 40;
|
||||||
|
if(this.isFast) standardTickDelay = 20;
|
||||||
|
|
||||||
|
TaskSchedule nextTick = TaskSchedule.tick(Math.round((float) standardTickDelay /this.level));
|
||||||
|
if(this.paused) return nextTick;
|
||||||
|
this.tick();
|
||||||
|
return nextTick;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateInfo();
|
||||||
|
this.nextTetrominoes.forEach(tetromino -> {
|
||||||
|
double xChange = -tetromino.getXChange();
|
||||||
|
tetromino.setPosition(this.nextPosition.sub(xChange, 4*this.nextTetrominoes.indexOf(tetromino), 0));
|
||||||
|
tetromino.drawAsEntities();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generate() {
|
||||||
|
this.playfield.generate();
|
||||||
|
|
||||||
|
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
|
||||||
|
this.currentTetromino.draw(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
if(this.lost || this.paused) return;
|
||||||
|
if(!currentTetromino.moveDown()) {
|
||||||
|
setActiveTetrominoDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getScore() {
|
||||||
|
return this.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateOtherTetrisGames(Collection<TetrisGame> tetrisGames) {
|
||||||
|
List<TetrisGame> games = new ArrayList<>(tetrisGames);
|
||||||
|
games.remove(this);
|
||||||
|
games.removeIf(tetrisGame -> tetrisGame.lost);
|
||||||
|
this.otherTetrisGames.clear();
|
||||||
|
this.otherTetrisGames.addAll(games);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAttacked(int lines) {
|
||||||
|
if(this.hasCombat && !this.lost) {
|
||||||
|
this.attackingLines += lines;
|
||||||
|
this.playfield.updateAttackingLines(this.attackingLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loose() {
|
||||||
|
this.lost = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean moveDown() {
|
||||||
|
if(!this.currentTetromino.moveDown()) {
|
||||||
|
this.setActiveTetrominoDown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.score += 1;
|
||||||
|
this.updateInfo();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hardDrop() {
|
||||||
|
if(!this.currentTetromino.moveDown()) {
|
||||||
|
this.setActiveTetrominoDown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.score += 2;
|
||||||
|
this.updateInfo();
|
||||||
|
while(this.currentTetromino.moveDown()) {
|
||||||
|
this.score += 2;
|
||||||
|
this.updateInfo();
|
||||||
|
}
|
||||||
|
this.setActiveTetrominoDown();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean switchHold() {
|
||||||
|
if(!holdPossible) return false;
|
||||||
|
|
||||||
|
Tetromino newCurrentTetromino;
|
||||||
|
if(this.holdTetromino == null) {
|
||||||
|
newCurrentTetromino = this.nextTetrominoes.removeFirst();
|
||||||
|
newCurrentTetromino.remove();
|
||||||
|
this.updateNextTetrominoes();
|
||||||
|
} else {
|
||||||
|
newCurrentTetromino = this.holdTetromino;
|
||||||
|
this.holdTetromino.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentTetromino.remove();
|
||||||
|
this.holdTetromino = new Tetromino(this.instance, this.currentTetromino.getShape());
|
||||||
|
this.currentTetromino = newCurrentTetromino;
|
||||||
|
|
||||||
|
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
|
||||||
|
this.currentTetromino.draw();
|
||||||
|
if(!this.currentTetromino.moveDown()) loose();
|
||||||
|
|
||||||
|
double xChange = this.holdTetromino.getXChange();
|
||||||
|
this.holdTetromino.setPosition(this.holdPosition.add(xChange, 0, 0));
|
||||||
|
this.holdTetromino.drawAsEntities();
|
||||||
|
this.holdPossible = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void updateNextTetrominoes() {
|
||||||
|
if(this.tetrominoBag.isEmpty()) {
|
||||||
|
for(Tetromino.Shape shape : Tetromino.Shape.values()) {
|
||||||
|
this.tetrominoBag.add(new Tetromino(this.instance, shape));
|
||||||
|
}
|
||||||
|
Collections.shuffle(this.tetrominoBag, this.random);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.nextTetrominoes.isEmpty()) this.nextTetrominoes.forEach(Tetromino::remove);
|
||||||
|
Tetromino newTetromino = this.tetrominoBag.removeFirst();
|
||||||
|
this.nextTetrominoes.add(newTetromino);
|
||||||
|
this.nextTetrominoes.forEach(tetromino -> {
|
||||||
|
double xChange = -tetromino.getXChange();
|
||||||
|
tetromino.setPosition(this.nextPosition.sub(xChange, 4*this.nextTetrominoes.indexOf(tetromino), 0));
|
||||||
|
tetromino.drawAsEntities();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildSidebar() {
|
||||||
|
this.sidebar.createLine(new Sidebar.ScoreboardLine(
|
||||||
|
"0",
|
||||||
|
Component.text("Score: "),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
this.sidebar.createLine(new Sidebar.ScoreboardLine(
|
||||||
|
"1",
|
||||||
|
Component.text("Lines: "),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
this.sidebar.createLine(new Sidebar.ScoreboardLine(
|
||||||
|
"2",
|
||||||
|
Component.text("Level: "),
|
||||||
|
1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateInfo() {
|
||||||
|
this.playfield.updateScore(this.score);
|
||||||
|
this.sidebar.updateLineScore("0", this.score);
|
||||||
|
this.sidebar.updateLineScore("1", this.lines);
|
||||||
|
this.sidebar.updateLineScore("2", this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setActiveTetrominoDown() {
|
||||||
|
this.currentTetromino.removeOwnEntities();
|
||||||
|
this.currentTetromino = this.nextTetrominoes.removeFirst();
|
||||||
|
this.currentTetromino.remove();
|
||||||
|
this.updateNextTetrominoes();
|
||||||
|
|
||||||
|
int removedLines = this.playfield.removeFullLines();
|
||||||
|
int combatLines = 0;
|
||||||
|
this.combo += 1;
|
||||||
|
switch (removedLines) {
|
||||||
|
case 0 -> this.combo = 0;
|
||||||
|
case 1 -> {
|
||||||
|
this.lines += 1;
|
||||||
|
this.score += 40 * this.level;
|
||||||
|
}
|
||||||
|
case 2 -> {
|
||||||
|
combatLines = 1;
|
||||||
|
this.lines += 2;
|
||||||
|
this.score += 100 * this.level;
|
||||||
|
}
|
||||||
|
case 3 -> {
|
||||||
|
combatLines = 2;
|
||||||
|
this.lines += 3;
|
||||||
|
this.score += 300 * this.level;
|
||||||
|
}
|
||||||
|
case 4 -> {
|
||||||
|
combatLines = 4;
|
||||||
|
this.lines += 4;
|
||||||
|
this.score += 1200 * this.level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.score += 50 * this.combo * this.level;
|
||||||
|
if(this.combo >= 2) {
|
||||||
|
combatLines += (int) Math.floor((double) this.combo /2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.hasCombat && this.attackingLines > 0) {
|
||||||
|
if(combatLines > 0 && this.attackingLines >= combatLines) {
|
||||||
|
this.attackingLines -= combatLines;
|
||||||
|
combatLines = 0;
|
||||||
|
} else if(combatLines > 0) {
|
||||||
|
combatLines -= this.attackingLines;
|
||||||
|
this.attackingLines = 0;
|
||||||
|
} else {
|
||||||
|
this.playfield.addLines(this.attackingLines);
|
||||||
|
this.attackingLines = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playfield.updateAttackingLines(this.attackingLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.hasCombat && !this.otherTetrisGames.isEmpty()) {
|
||||||
|
Collections.shuffle(this.otherTetrisGames);
|
||||||
|
this.otherTetrisGames.getFirst().getAttacked(combatLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.level = (int) Math.floor((double) this.lines / 10) + 1;
|
||||||
|
this.holdPossible = true;
|
||||||
|
|
||||||
|
this.updateInfo();
|
||||||
|
|
||||||
|
this.currentTetromino.setPosition(this.tetrominoSpawnPosition);
|
||||||
|
this.currentTetromino.draw();
|
||||||
|
if(!this.currentTetromino.moveDown()) {
|
||||||
|
loose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,246 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||||
|
import net.minestom.server.coordinate.Pos;
|
||||||
|
import net.minestom.server.entity.Entity;
|
||||||
|
import net.minestom.server.entity.EntityType;
|
||||||
|
import net.minestom.server.entity.metadata.other.FallingBlockMeta;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.tag.Tag;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Tetromino {
|
||||||
|
private final Shape shape;
|
||||||
|
private final StatelessGame instance;
|
||||||
|
private Pos position;
|
||||||
|
private int[][] shapeArray;
|
||||||
|
private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK;
|
||||||
|
private final UUID uuid;
|
||||||
|
private final static Tag<String> uuidTag = Tag.String("uuid");
|
||||||
|
|
||||||
|
public enum Shape {
|
||||||
|
I,
|
||||||
|
J,
|
||||||
|
L,
|
||||||
|
O,
|
||||||
|
S,
|
||||||
|
T,
|
||||||
|
Z
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tetromino(StatelessGame instance, Shape shape) {
|
||||||
|
this.instance = instance;
|
||||||
|
this.shape = shape;
|
||||||
|
this.uuid = UUID.randomUUID();
|
||||||
|
|
||||||
|
switch (this.shape) {
|
||||||
|
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}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EntityType getGhostEntityType() {
|
||||||
|
return ghostEntityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPosition(Pos newPosition) {
|
||||||
|
this.position = newPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean rotate(boolean clockwise) {
|
||||||
|
int[][] newShapeArray = this.getTurnedShapeArray(clockwise);
|
||||||
|
return checkCollisionAndMove(this.position, newShapeArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean moveDown() {
|
||||||
|
Pos newPosition = this.position.sub(0, 1, 0);
|
||||||
|
return checkCollisionAndMove(newPosition, this.shapeArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean moveLeft() {
|
||||||
|
Pos newPosition = this.position.sub(1, 0, 0);
|
||||||
|
return checkCollisionAndMove(newPosition, this.shapeArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean moveRight() {
|
||||||
|
Pos newPosition = this.position.add(1, 0, 0);
|
||||||
|
return checkCollisionAndMove(newPosition, this.shapeArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void draw() {
|
||||||
|
this.draw(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void draw(boolean withGhost) {
|
||||||
|
if(withGhost) {
|
||||||
|
Pos ghostPos = this.position;
|
||||||
|
while (!checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) {
|
||||||
|
ghostPos = ghostPos.sub(0, 1, 0);
|
||||||
|
}
|
||||||
|
Pos positionChange = this.position.sub(ghostPos);
|
||||||
|
getBlockPositions().forEach(pos -> {
|
||||||
|
Entity ghostBlock = new Entity(ghostEntityType);
|
||||||
|
((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getGhostBlock());
|
||||||
|
ghostBlock.setNoGravity(true);
|
||||||
|
ghostBlock.setGlowing(true);
|
||||||
|
ghostBlock.setTag(uuidTag, this.uuid.toString());
|
||||||
|
ghostBlock.setInstance(this.instance, pos.sub(positionChange).add(0.5, 0, 0.5));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drawAsEntities() {
|
||||||
|
getBlockPositions().forEach(pos -> {
|
||||||
|
Entity ghostBlock = new Entity(ghostEntityType);
|
||||||
|
((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getColoredBlock());
|
||||||
|
ghostBlock.setNoGravity(true);
|
||||||
|
ghostBlock.setTag(uuidTag, this.uuid.toString());
|
||||||
|
ghostBlock.setInstance(this.instance, pos.add(0.5, 0, 0.5));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove() {
|
||||||
|
this.removeOwnEntities();
|
||||||
|
this.getBlockPositions().forEach(pos -> this.instance.setBlock(pos, Block.AIR));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Block getColoredBlock() {
|
||||||
|
Block returnBlock;
|
||||||
|
switch (this.shape) {
|
||||||
|
case I -> returnBlock = Block.LIGHT_BLUE_CONCRETE;
|
||||||
|
case J -> returnBlock = Block.BLUE_CONCRETE;
|
||||||
|
case L -> returnBlock = Block.ORANGE_CONCRETE;
|
||||||
|
case O -> returnBlock = Block.YELLOW_CONCRETE;
|
||||||
|
case S -> returnBlock = Block.GREEN_CONCRETE;
|
||||||
|
case T -> returnBlock = Block.PURPLE_CONCRETE;
|
||||||
|
case Z -> returnBlock = Block.RED_CONCRETE;
|
||||||
|
default -> returnBlock = Block.WHITE_CONCRETE;
|
||||||
|
}
|
||||||
|
return returnBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeOwnEntities() {
|
||||||
|
this.instance.getEntities().stream()
|
||||||
|
.filter(entity -> {
|
||||||
|
String tagValue = entity.getTag(uuidTag);
|
||||||
|
if(tagValue == null) return false;
|
||||||
|
return entity.getTag(uuidTag).equals(this.uuid.toString());
|
||||||
|
})
|
||||||
|
.forEach(Entity::remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getXChange() {
|
||||||
|
switch (this.shape) {
|
||||||
|
case O, I -> {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case null, default -> {
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Shape getShape() {
|
||||||
|
return this.shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Block getGhostBlock() {
|
||||||
|
Block returnBlock;
|
||||||
|
switch (this.shape) {
|
||||||
|
case I -> returnBlock = Block.LIGHT_BLUE_STAINED_GLASS;
|
||||||
|
case J -> returnBlock = Block.BLUE_STAINED_GLASS;
|
||||||
|
case L -> returnBlock = Block.ORANGE_STAINED_GLASS;
|
||||||
|
case O -> returnBlock = Block.YELLOW_STAINED_GLASS;
|
||||||
|
case S -> returnBlock = Block.GREEN_STAINED_GLASS;
|
||||||
|
case T -> returnBlock = Block.PURPLE_STAINED_GLASS;
|
||||||
|
case Z -> returnBlock = Block.RED_STAINED_GLASS;
|
||||||
|
default -> returnBlock = Block.WHITE_STAINED_GLASS;
|
||||||
|
}
|
||||||
|
return returnBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[][] getTurnedShapeArray(boolean clockwise) {
|
||||||
|
int iterations = 1;
|
||||||
|
if(!clockwise) iterations = 3;
|
||||||
|
|
||||||
|
int arrayLength = this.shapeArray.length;
|
||||||
|
int[][] startArray = Arrays.stream(this.shapeArray).map(int[]::clone).toArray(int[][]::new);
|
||||||
|
int[][] returnArray = new int[arrayLength][arrayLength];
|
||||||
|
|
||||||
|
for(int k=0; k<iterations; k++) {
|
||||||
|
for(int i=0; i<arrayLength; i++) {
|
||||||
|
for(int j=0; j<arrayLength; j++) {
|
||||||
|
returnArray[i][arrayLength-1-j] = startArray[j][i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startArray = Arrays.stream(returnArray).map(int[]::clone).toArray(int[][]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPartOfTetromino(Pos position) {
|
||||||
|
return this.getBlockPositions().stream()
|
||||||
|
.anyMatch(pos -> pos.equals(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Pos> getBlockPositions() {
|
||||||
|
return this.getBlockPositions(this.position, this.shapeArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Pos> getBlockPositions(Pos position, int[][] shapeArray) {
|
||||||
|
List<Pos> returnList = new ArrayList<>();
|
||||||
|
if(position == null) return returnList;
|
||||||
|
|
||||||
|
int arrayLength = shapeArray.length;
|
||||||
|
|
||||||
|
for(int x=0; x<arrayLength; x++) {
|
||||||
|
for(int y=0; y<arrayLength; y++) {
|
||||||
|
if(shapeArray[arrayLength-1-y][x] == 1) {
|
||||||
|
switch (this.shape) {
|
||||||
|
case I -> returnList.add(position.add(x-1, y-2, 0));
|
||||||
|
case O -> returnList.add(position.add(x, y, 0));
|
||||||
|
default -> returnList.add(position.add(x-1, y-1, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkCollision(Pos newPosition, int[][] newShapeArray) {
|
||||||
|
List<Pos> newBlockPositions = getBlockPositions(newPosition, newShapeArray);
|
||||||
|
|
||||||
|
for(Pos pos : newBlockPositions) {
|
||||||
|
if(isPartOfTetromino(pos)) continue;
|
||||||
|
if(this.instance.getBlock(pos) == this.getGhostBlock()) continue;
|
||||||
|
if(this.instance.getBlock(pos) != Block.AIR) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) {
|
||||||
|
if(!checkCollision(newPosition, newShapeArray)) {
|
||||||
|
this.remove();
|
||||||
|
this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new);
|
||||||
|
this.setPosition(newPosition);
|
||||||
|
this.draw();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ import java.util.Set;
|
|||||||
public class LastWinsScore extends Score {
|
public class LastWinsScore extends Score {
|
||||||
@Override
|
@Override
|
||||||
public void insertResultImplementation(Set<Player> p) {
|
public void insertResultImplementation(Set<Player> p) {
|
||||||
getScores().add(0, p);
|
getScores().addFirst(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.score;
|
||||||
|
|
||||||
|
import eu.mhsl.minenet.minigames.message.Icon;
|
||||||
|
import eu.mhsl.minenet.minigames.message.TranslatableMessage;
|
||||||
|
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
|
||||||
|
import eu.mhsl.minenet.minigames.message.type.TitleMessage;
|
||||||
|
import eu.mhsl.minenet.minigames.util.MapUtil;
|
||||||
|
import net.minestom.server.entity.Player;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class PointsWinScore extends Score {
|
||||||
|
private Map<Set<Player>, Integer> scoreOrder = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void insertResultImplementation(Set<Player> p, int currentPoints) {
|
||||||
|
this.scoreOrder.put(p, currentPoints);
|
||||||
|
this.scoreOrder = MapUtil.sortByValue(this.scoreOrder);
|
||||||
|
getScores().clear();
|
||||||
|
this.scoreOrder.forEach((player, integer) -> getScores().addFirst(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void insertResultImplementation(Set<Player> p) {
|
||||||
|
this.insertResultImplementation(p, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDone() {
|
||||||
|
if(isClosed()) return;
|
||||||
|
close();
|
||||||
|
new ChatMessage(Icon.STAR, true)
|
||||||
|
.appendTranslated("score#result")
|
||||||
|
.newLine()
|
||||||
|
.indent()
|
||||||
|
.numberedList(
|
||||||
|
getScores()
|
||||||
|
.stream()
|
||||||
|
.filter(players -> !players.stream()
|
||||||
|
.filter(player -> !player.getUsername().isBlank())
|
||||||
|
.toList()
|
||||||
|
.isEmpty())
|
||||||
|
.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()
|
||||||
|
.appendTranslated("score#thanks")
|
||||||
|
.send(instance.getPlayers());
|
||||||
|
|
||||||
|
instance.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TranslatableMessage scoreMessage() {
|
||||||
|
return new TitleMessage(Duration.ofMillis(1000), Duration.ofSeconds(1)).appendTranslated("score#death");
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import net.minestom.server.entity.Player;
|
|||||||
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
|
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
|
||||||
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
|
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
|
||||||
import net.minestom.server.event.player.PlayerDisconnectEvent;
|
import net.minestom.server.event.player.PlayerDisconnectEvent;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -22,10 +23,19 @@ public abstract class Score {
|
|||||||
|
|
||||||
public Score() {}
|
public Score() {}
|
||||||
|
|
||||||
public Score(int ignoreLastPlayers) {
|
protected abstract void insertResultImplementation(Set<Player> p);
|
||||||
this.ignoreLastPlayers = ignoreLastPlayers;
|
|
||||||
|
protected void insertResultImplementation(Set<Player> p, int points) {
|
||||||
|
throw new NotImplementedException("This Score type is not able to process points");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void insertResult(Player p) {
|
||||||
|
this.insertResultProcessor(p, () -> this.insertResultImplementation(Set.of(p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertResult(Player p, int points) {
|
||||||
|
this.insertResultProcessor(p, () -> this.insertResultImplementation(Set.of(p), points));
|
||||||
|
}
|
||||||
|
|
||||||
public void attachListeners() {
|
public void attachListeners() {
|
||||||
this.instance.eventNode()
|
this.instance.eventNode()
|
||||||
@ -45,15 +55,8 @@ public abstract class Score {
|
|||||||
setDone();
|
setDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected abstract void insertResultImplementation(Set<Player> p);
|
|
||||||
protected abstract TranslatableMessage scoreMessage();
|
|
||||||
|
|
||||||
public void insertResult(Player p) {
|
protected abstract TranslatableMessage scoreMessage();
|
||||||
if(hasResult(p)) return;
|
|
||||||
this.scoreMessage().send(p);
|
|
||||||
this.insertResultImplementation(Set.of(p));
|
|
||||||
this.checkGameEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insertRemainingPlayers(Set<Player> players) {
|
public void insertRemainingPlayers(Set<Player> players) {
|
||||||
this.insertResultImplementation(players.stream().filter(p -> !hasResult(p)).collect(Collectors.toSet()));
|
this.insertResultImplementation(players.stream().filter(p -> !hasResult(p)).collect(Collectors.toSet()));
|
||||||
@ -93,10 +96,19 @@ public abstract class Score {
|
|||||||
instance.stop();
|
instance.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void insertResultProcessor(Player p, Runnable callback) {
|
||||||
|
if(hasResult(p)) return;
|
||||||
|
this.scoreMessage().send(p);
|
||||||
|
callback.run();
|
||||||
|
this.checkGameEnd();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isClosed() {
|
public boolean isClosed() {
|
||||||
return isClosed;
|
return isClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void close() { isClosed = true; }
|
||||||
|
|
||||||
protected void onGameEnd() {
|
protected void onGameEnd() {
|
||||||
this.instance.stop();
|
this.instance.stop();
|
||||||
}
|
}
|
||||||
|
20
src/main/java/eu/mhsl/minenet/minigames/util/MapUtil.java
Normal file
20
src/main/java/eu/mhsl/minenet/minigames/util/MapUtil.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package eu.mhsl.minenet.minigames.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) {
|
||||||
|
List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
|
||||||
|
list.sort(Map.Entry.comparingByValue());
|
||||||
|
|
||||||
|
Map<K, V> result = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<K, V> entry : list) {
|
||||||
|
result.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -17,4 +17,5 @@ api:
|
|||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
admins:
|
admins:
|
||||||
- minetec
|
- minetec
|
||||||
|
- 28Pupsi28
|
@ -92,6 +92,13 @@ name;Spleef;Spleef;
|
|||||||
description;Spleef other players and be the last survivor;Zerstöre Blöcke unter anderen Spielern und sei der letzte im Feld
|
description;Spleef other players and be the last survivor;Zerstöre Blöcke unter anderen Spielern und sei der letzte im Feld
|
||||||
shovelName;Snow thrower;Schneeflug
|
shovelName;Snow thrower;Schneeflug
|
||||||
;;
|
;;
|
||||||
|
ns:game_Tetris#;;
|
||||||
|
name;Tetris;Tetris
|
||||||
|
description;Sort falling blocks and clear lines;Sortiere fallende Blöcke und kläre Linien
|
||||||
|
nextTetrominoesCount;Number of upcoming Tetrominos displayed;Anzahl der angezeigten kommenden Tetrominos
|
||||||
|
isFast;Fast mode;Schneller Modus
|
||||||
|
hasCombat;Competitive mode;Kompetitiver Modus
|
||||||
|
;;
|
||||||
ns:game_AnvilRun#;;
|
ns:game_AnvilRun#;;
|
||||||
name;Anvil Run;Anvil Run
|
name;Anvil Run;Anvil Run
|
||||||
description;Run away from falling anvils;Renne von fallenden Ambossen davon
|
description;Run away from falling anvils;Renne von fallenden Ambossen davon
|
Can't render this file because it has a wrong number of fields in line 91.
|
Loading…
x
Reference in New Issue
Block a user