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:
Lars Neuhaus 2024-12-16 21:02:50 +01:00
commit 0a1ae69f53
17 changed files with 1126 additions and 27 deletions

View File

@ -11,6 +11,7 @@ import eu.mhsl.minenet.minigames.instance.Spawnable;
import eu.mhsl.minenet.minigames.instance.room.Room;
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.item.ItemDropEvent;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
@ -98,7 +99,11 @@ public abstract class Game extends MineNetInstance implements Spawnable {
public void unload() {
this.onUnload();
getPlayers().forEach(Room::setOwnRoom);
getPlayers().forEach(player -> {
Room.setOwnRoom(player);
player.setGameMode(GameMode.SURVIVAL);
player.setInvisible(false);
});
scheduler().scheduleTask(() -> {

View File

@ -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.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.tntrun.TntRunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace.TrafficLightRaceFactory;
@ -23,9 +24,10 @@ public enum GameList {
TOWERDEFENSE(new TowerdefenseFactory(), GameType.PROTOTYPE),
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE),
ANVILRUN(new AnvilRunFactory(), GameType.PROTOTYPE),
TETRIS(new TetrisFactory(), GameType.OTHER),
TNTRUN(new TntRunFactory(), GameType.OTHER),
ACIDRAIN(new AcidRainFactory(), GameType.PVE),
ANVILRUN(new AnvilRunFactory(), GameType.PVE),
ELYTRARACE(new ElytraRaceFactory(), GameType.PVP),
SPLEEF(new SpleefFactory(), GameType.PVP),
BOWSPLEEF(new BowSpleefFactory(), GameType.PVP);

View File

@ -50,8 +50,8 @@ public class StatelessGame extends Game {
int timeLeft = timeLimit - timePlayed;
switch (timeLeft) {
case 60, 30, 10, 5, 4, 3, 2, 1 ->
new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers());
case 90, 60, 30, 10, 5, 4, 3, 2, 1 ->
new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers());
}
timePlayed++;

View File

@ -9,6 +9,7 @@ import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import java.util.List;
import java.util.Objects;
public abstract class Option<T> {
private RestrictionHandler restrictionHandler;
@ -25,7 +26,7 @@ public abstract class Option<T> {
this.name = name;
this.options = options;
currentValue = options.get(0);
currentValue = options.getFirst();
}
public void setRestrictionHandler(RestrictionHandler restrictionHandler) {
@ -44,11 +45,11 @@ public abstract class Option<T> {
}
public ItemStack getCurrent(Player p) {
int amount = Integer.parseInt(options.get(pointer).toString());
String value = options.get(pointer).toString();
return ItemStack.builder(item)
.customName(name.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();
}
@ -56,6 +57,10 @@ public abstract class Option<T> {
return Integer.parseInt(getAsString());
}
public boolean getAsBoolean() {
return Objects.equals(getAsString(), "true") || Objects.equals(getAsString(), "1");
}
public String getAsString() {
return currentValue.toString();
}

View File

@ -6,8 +6,8 @@ import net.minestom.server.item.Material;
import java.util.List;
public class BoolOption extends Option<Boolean> {
public class BoolOption extends Option<String> {
public BoolOption(String id, Material item, TranslatedComponent name) {
super(id, item, name, List.of(true, false));
super(id, item, name, List.of("true", "false"));
}
}

View File

@ -40,6 +40,7 @@ public class ElytraRace extends StatelessGame {
private final ValeGenerator vale = new ValeGenerator();
private final int gameHeight = 0;
private final int seaLevel = -55;
private final int ringSpacing = 100;
private final int ringCount;
@ -62,7 +63,7 @@ public class ElytraRace extends StatelessGame {
this.ringCount = ringCount;
setGenerator(vale);
vale.setCalculateSeaLevel(point -> -55);
vale.setCalculateSeaLevel(point -> seaLevel);
vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1));
vale.addMixIn(new PlaneTerrainGenerator(gameHeight, Block.BARRIER));
@ -106,9 +107,7 @@ public class ElytraRace extends StatelessGame {
@Override
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
Point spawnpoint = new Pos(vale.getXShiftAtZ(0), -46, 0);
GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> {
setBlock(point, BlockPallet.STREET.rnd());
});
GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> setBlock(point, BlockPallet.STREET.rnd()));
generateRing(ringSpacing);
generateRing(ringSpacing * 2);
@ -195,8 +194,8 @@ public class ElytraRace extends StatelessGame {
Point ringPos = getRingPositionAtZ(zPos);
GeneratorUtils.iterateArea(
ringPos.sub(100, 0, 0).withY(0), // TODO 0 was before update getDimensionType().getMinY, might not work correctly
ringPos.add(100, 0, 0).withY(gameHeight),
ringPos.sub(100, 0, 0).withY(0),
ringPos.add(100, 0, 0).withY(seaLevel),
point -> batch.setBlock(point, Block.BARRIER)
);
GeneratorUtils.iterateArea(

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -10,7 +10,7 @@ import java.util.Set;
public class LastWinsScore extends Score {
@Override
public void insertResultImplementation(Set<Player> p) {
getScores().add(0, p);
getScores().addFirst(p);
}
@Override

View File

@ -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");
}
}

View File

@ -8,6 +8,7 @@ import net.minestom.server.entity.Player;
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
import net.minestom.server.event.player.PlayerDisconnectEvent;
import org.apache.commons.lang3.NotImplementedException;
import java.util.ArrayList;
import java.util.List;
@ -22,10 +23,19 @@ public abstract class Score {
public Score() {}
public Score(int ignoreLastPlayers) {
this.ignoreLastPlayers = ignoreLastPlayers;
protected abstract void insertResultImplementation(Set<Player> p);
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() {
this.instance.eventNode()
@ -45,15 +55,8 @@ public abstract class Score {
setDone();
}
}
protected abstract void insertResultImplementation(Set<Player> p);
protected abstract TranslatableMessage scoreMessage();
public void insertResult(Player p) {
if(hasResult(p)) return;
this.scoreMessage().send(p);
this.insertResultImplementation(Set.of(p));
this.checkGameEnd();
}
protected abstract TranslatableMessage scoreMessage();
public void insertRemainingPlayers(Set<Player> players) {
this.insertResultImplementation(players.stream().filter(p -> !hasResult(p)).collect(Collectors.toSet()));
@ -93,10 +96,19 @@ public abstract class Score {
instance.stop();
}
private void insertResultProcessor(Player p, Runnable callback) {
if(hasResult(p)) return;
this.scoreMessage().send(p);
callback.run();
this.checkGameEnd();
}
public boolean isClosed() {
return isClosed;
}
public void close() { isClosed = true; }
protected void onGameEnd() {
this.instance.stop();
}

View 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;
}
}

View File

@ -17,4 +17,5 @@ api:
port: 8080
admins:
- minetec
- minetec
- 28Pupsi28

View File

@ -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
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#;;
name;Anvil Run;Anvil Run
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.