diff --git a/build.gradle b/build.gradle index 8dd736d..d148f3d 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,7 @@ dependencies { modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}") implementation 'com.github.bhlangonijr:chesslib:1.3.6' + include 'com.github.bhlangonijr:chesslib:1.3.3' } processResources { diff --git a/src/client/java/dev/tggamesyt/szar/client/ChessScreen.java b/src/client/java/dev/tggamesyt/szar/client/ChessScreen.java new file mode 100644 index 0000000..5041380 --- /dev/null +++ b/src/client/java/dev/tggamesyt/szar/client/ChessScreen.java @@ -0,0 +1,294 @@ +package dev.tggamesyt.szar.client; + +import com.github.bhlangonijr.chesslib.Board; +import com.github.bhlangonijr.chesslib.Piece; +import com.github.bhlangonijr.chesslib.Side; +import com.github.bhlangonijr.chesslib.Square; +import com.github.bhlangonijr.chesslib.move.Move; +import dev.tggamesyt.szar.ChessBlockEntity; +import dev.tggamesyt.szar.Szar; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; + +import java.util.*; + +public class ChessScreen extends Screen { + + // Board background texture + private static final Identifier BOARD_TEX = + new Identifier("szar", "textures/gui/chess_board.png"); + + // Piece textures — one per piece type + // Naming: chess_wp.png (white pawn), chess_bn.png (black knight) etc. + private static final Map PIECE_TEXTURES = new HashMap<>(); + + static { + String[] colors = {"w", "b"}; + String[] types = {"p", "n", "b", "r", "q", "k"}; + Piece[] whitePieces = {Piece.WHITE_PAWN, Piece.WHITE_KNIGHT, Piece.WHITE_BISHOP, + Piece.WHITE_ROOK, Piece.WHITE_QUEEN, Piece.WHITE_KING}; + Piece[] blackPieces = {Piece.BLACK_PAWN, Piece.BLACK_KNIGHT, Piece.BLACK_BISHOP, + Piece.BLACK_ROOK, Piece.BLACK_QUEEN, Piece.BLACK_KING}; + + for (int i = 0; i < 6; i++) { + PIECE_TEXTURES.put(whitePieces[i], + new Identifier("szar", "textures/gui/chess_w" + types[i] + ".png")); + PIECE_TEXTURES.put(blackPieces[i], + new Identifier("szar", "textures/gui/chess_b" + types[i] + ".png")); + } + } + + private static final int CELL = 24; // pixels per square + private static final int BOARD_PIXELS = CELL * 8; // 192 + private static final int GUI_WIDTH = BOARD_PIXELS + 16; + private static final int GUI_HEIGHT = BOARD_PIXELS + 32; + private static final int BOARD_OFFSET_X = 8; + private static final int BOARD_OFFSET_Y = 8; + + // Colors + private static final int LIGHT_SQUARE = 0xFFF0D9B5; + private static final int DARK_SQUARE = 0xFFB58863; + private static final int SELECTED = 0xAA7FC97F; + private static final int VALID_MOVE = 0xAA7FC97F; + private static final int LAST_MOVE = 0xAA99CC66; + + private ChessBlockEntity.State state; + private final BlockPos blockPos; + private final UUID localPlayer; + private boolean isSpectator; + + private Square selectedSquare = null; + private Set validMoveTargets = new HashSet<>(); + + public ChessScreen(BlockPos pos, ChessBlockEntity.State state) { + super(Text.literal("Chess")); + this.blockPos = pos; + this.state = state; + this.localPlayer = MinecraftClient.getInstance().player.getUuid(); + this.isSpectator = state.isSpectator; + } + + public void updateState(ChessBlockEntity.State newState) { + this.state = newState; + this.isSpectator = newState.isSpectator; + // Clear selection on state update + selectedSquare = null; + validMoveTargets.clear(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + renderBackground(context); + + int bx = (this.width - GUI_WIDTH) / 2 + BOARD_OFFSET_X; + int by = (this.height - GUI_HEIGHT) / 2 + BOARD_OFFSET_Y; + + Board board = new Board(); + board.loadFromFen(state.fen); + + // Draw squares + for (int rank = 0; rank < 8; rank++) { + for (int file = 0; file < 8; file++) { + int drawFile = state.isWhite ? file : (7 - file); + int drawRank = state.isWhite ? (7 - rank) : rank; + + int sx = bx + drawFile * CELL; + int sy = by + drawRank * CELL; + + boolean light = (file + rank) % 2 == 0; + int squareColor = light ? LIGHT_SQUARE : DARK_SQUARE; + + // Check if selected or valid move target + Square sq = getSquare(file, rank); + if (sq != null && sq.equals(selectedSquare)) { + squareColor = SELECTED; + } else if (sq != null && validMoveTargets.contains(sq)) { + squareColor = VALID_MOVE; + } + + context.fill(sx, sy, sx + CELL, sy + CELL, squareColor); + } + } + + // Draw pieces + for (int rank = 0; rank < 8; rank++) { + for (int file = 0; file < 8; file++) { + Square sq = getSquare(file, rank); + if (sq == null) continue; + + Piece piece = board.getPiece(sq); + if (piece == null || piece == Piece.NONE) continue; + + Identifier tex = PIECE_TEXTURES.get(piece); + if (tex == null) continue; + + int drawFile = state.isWhite ? file : (7 - file); + int drawRank = state.isWhite ? (7 - rank) : rank; + + int sx = bx + drawFile * CELL + 1; + int sy = by + drawRank * CELL + 1; + + context.drawTexture(tex, sx, sy, 0, 0, + CELL - 2, CELL - 2, CELL - 2, CELL - 2); + } + } + + // Draw rank/file labels + for (int i = 0; i < 8; i++) { + String fileLabel = String.valueOf((char)('a' + (state.isWhite ? i : 7 - i))); + String rankLabel = String.valueOf(state.isWhite ? 8 - i : i + 1); + context.drawText(this.textRenderer, fileLabel, + bx + i * CELL + CELL / 2 - 3, + by + BOARD_PIXELS + 2, 0xFFFFFF, true); + context.drawText(this.textRenderer, rankLabel, + bx - 7, by + i * CELL + CELL / 2 - 4, + 0xFFFFFF, true); + } + + // Status text + String status; + if (state.winner == 1) status = "§fWhite wins!"; + else if (state.winner == 2) status = "§8Black wins!"; + else if (state.winner == 3) status = "§7Draw! " + state.statusMessage; + else if (!state.statusMessage.isEmpty()) status = "§e" + state.statusMessage; + else if (isSpectator) status = "§7Spectating..."; + else { + Side sideToMove = board.getSideToMove(); + boolean myTurn = (sideToMove == Side.WHITE && localPlayer.equals(state.player1)) + || (sideToMove == Side.BLACK && localPlayer.equals(state.player2)); + status = myTurn ? "§aYour turn!" : "§7Opponent's turn..."; + } + + int statusX = (this.width - GUI_WIDTH) / 2 + GUI_WIDTH / 2 + - this.textRenderer.getWidth(status) / 2; + int statusY = (this.height - GUI_HEIGHT) / 2 + BOARD_OFFSET_Y + + BOARD_PIXELS + 16; + context.drawTextWithShadow(this.textRenderer, Text.literal(status), + statusX, statusY, 0xFFFFFF); + + super.render(context, mouseX, mouseY, delta); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (isSpectator || button != 0 || state.winner != 0) + return super.mouseClicked(mouseX, mouseY, button); + + Board board = new Board(); + board.loadFromFen(state.fen); + + Side sideToMove = board.getSideToMove(); + boolean myTurn = (sideToMove == Side.WHITE && localPlayer.equals(state.player1)) + || (sideToMove == Side.BLACK && localPlayer.equals(state.player2)); + if (!myTurn) return super.mouseClicked(mouseX, mouseY, button); + + int bx = (this.width - GUI_WIDTH) / 2 + BOARD_OFFSET_X; + int by = (this.height - GUI_HEIGHT) / 2 + BOARD_OFFSET_Y; + + // Check if click is on the board + if (mouseX < bx || mouseX >= bx + BOARD_PIXELS + || mouseY < by || mouseY >= by + BOARD_PIXELS) + return super.mouseClicked(mouseX, mouseY, button); + + int drawFile = (int)((mouseX - bx) / CELL); + int drawRank = (int)((mouseY - by) / CELL); + + // Convert draw coords back to board coords + int file = state.isWhite ? drawFile : (7 - drawFile); + int rank = state.isWhite ? (7 - drawRank) : drawRank; + + Square clicked = getSquare(file, rank); + if (clicked == null) return super.mouseClicked(mouseX, mouseY, button); + + if (selectedSquare == null) { + // Select a piece + Piece piece = board.getPiece(clicked); + if (piece != Piece.NONE) { + boolean ownPiece = (sideToMove == Side.WHITE && piece.getPieceSide() == Side.WHITE) + || (sideToMove == Side.BLACK && piece.getPieceSide() == Side.BLACK); + if (ownPiece) { + selectedSquare = clicked; + // Calculate valid moves for this piece + validMoveTargets.clear(); + for (Move move : board.legalMoves()) { + if (move.getFrom().equals(clicked)) { + validMoveTargets.add(move.getTo()); + } + } + } + } + } else { + if (clicked.equals(selectedSquare)) { + // Deselect + selectedSquare = null; + validMoveTargets.clear(); + } else if (validMoveTargets.contains(clicked)) { + // Make the move + String uci = selectedSquare.value().toLowerCase() + + clicked.value().toLowerCase(); + + // Auto-promote to queen if pawn reaches last rank + Piece moving = board.getPiece(selectedSquare); + if (moving == Piece.WHITE_PAWN && clicked.getRank().ordinal() == 7) { + uci += "q"; + } else if (moving == Piece.BLACK_PAWN && clicked.getRank().ordinal() == 0) { + uci += "q"; + } + + sendMove(uci); + selectedSquare = null; + validMoveTargets.clear(); + } else { + // Try selecting a different piece + Piece piece = board.getPiece(clicked); + if (piece != Piece.NONE) { + boolean ownPiece = (sideToMove == Side.WHITE && piece.getPieceSide() == Side.WHITE) + || (sideToMove == Side.BLACK && piece.getPieceSide() == Side.BLACK); + if (ownPiece) { + selectedSquare = clicked; + validMoveTargets.clear(); + for (Move move : board.legalMoves()) { + if (move.getFrom().equals(clicked)) { + validMoveTargets.add(move.getTo()); + } + } + } else { + selectedSquare = null; + validMoveTargets.clear(); + } + } else { + selectedSquare = null; + validMoveTargets.clear(); + } + } + } + + return true; + } + + private Square getSquare(int file, int rank) { + try { + String name = String.valueOf((char)('A' + file)) + (rank + 1); + return Square.valueOf(name); + } catch (Exception e) { + return null; + } + } + + private void sendMove(String uci) { + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeBlockPos(blockPos); + buf.writeString(uci); + ClientPlayNetworking.send(Szar.CHESS_MAKE_MOVE, buf); + } + + @Override + public boolean shouldPause() { return false; } +} \ No newline at end of file diff --git a/src/client/java/dev/tggamesyt/szar/client/SzarClient.java b/src/client/java/dev/tggamesyt/szar/client/SzarClient.java index b753bd5..1813c32 100644 --- a/src/client/java/dev/tggamesyt/szar/client/SzarClient.java +++ b/src/client/java/dev/tggamesyt/szar/client/SzarClient.java @@ -90,6 +90,35 @@ public class SzarClient implements ClientModInitializer { ); @Override public void onInitializeClient() { + ClientPlayNetworking.registerGlobalReceiver(Szar.CHESS_OPEN_SCREEN, (client, handler, buf, sender) -> { + BlockPos pos = buf.readBlockPos(); + ChessBlockEntity.State state = ChessBlockEntity.readStateFromBuf(buf); + client.execute(() -> { + if (client.currentScreen instanceof ChessScreen existing) { + existing.updateState(state); + } else { + client.setScreen(new ChessScreen(pos, state)); + } + }); + }); + + ClientPlayNetworking.registerGlobalReceiver(Szar.CHESS_STATE_SYNC, (client, handler, buf, sender) -> { + BlockPos pos = buf.readBlockPos(); + ChessBlockEntity.State state = ChessBlockEntity.readStateFromBuf(buf); + client.execute(() -> { + if (client.currentScreen instanceof ChessScreen screen) { + screen.updateState(state); + } + }); + }); + + ClientPlayNetworking.registerGlobalReceiver(Szar.CHESS_CLOSE_SCREEN, (client, handler, buf, sender) -> { + client.execute(() -> { + if (client.currentScreen instanceof ChessScreen) { + client.setScreen(null); + } + }); + }); // TTT ClientPlayNetworking.registerGlobalReceiver(Szar.TTT_OPEN_SCREEN, (client, handler, buf, sender) -> { BlockPos pos = buf.readBlockPos(); diff --git a/src/main/java/dev/tggamesyt/szar/ChessBlock.java b/src/main/java/dev/tggamesyt/szar/ChessBlock.java index 184a70f..cc34db0 100644 --- a/src/main/java/dev/tggamesyt/szar/ChessBlock.java +++ b/src/main/java/dev/tggamesyt/szar/ChessBlock.java @@ -1,4 +1,74 @@ package dev.tggamesyt.szar; -public class ChessBlock { +import net.minecraft.block.*; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public class ChessBlock extends BlockWithEntity { + + private static final VoxelShape SHAPE = VoxelShapes.union( + VoxelShapes.cuboid(0f, 0f, 0f, 1f, 0.75f, 1f) + ); + + public ChessBlock(Settings settings) { super(settings); } + + @Override + public BlockRenderType getRenderType(BlockState state) { + return BlockRenderType.MODEL; + } + + @Override + public VoxelShape getOutlineShape(BlockState state, BlockView world, + BlockPos pos, ShapeContext ctx) { return SHAPE; } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockView world, + BlockPos pos, ShapeContext ctx) { return SHAPE; } + + @Nullable + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return new ChessBlockEntity(pos, state); + } + + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, + PlayerEntity player, Hand hand, BlockHitResult hit) { + if (world.isClient) return ActionResult.SUCCESS; + if (!(player instanceof ServerPlayerEntity sp)) return ActionResult.PASS; + if (!(world.getBlockEntity(pos) instanceof ChessBlockEntity be)) return ActionResult.PASS; + be.handlePlayerJoin(sp, pos); + return ActionResult.SUCCESS; + } + + @Override + public BlockEntityTicker getTicker( + World world, BlockState state, BlockEntityType type) { + if (world.isClient) return null; + return type == Szar.CHESS_ENTITY + ? (w, pos, s, be) -> ChessBlockEntity.tick(w, pos, s, (ChessBlockEntity) be) + : null; + } + + @Override + public void onBreak(World world, BlockPos pos, BlockState state, PlayerEntity player) { + if (!world.isClient && world.getBlockEntity(pos) instanceof ChessBlockEntity be) { + be.closeScreenForAll(world.getServer()); + if (be.player1 != null) Szar.chessActivePlayers.remove(be.player1); + if (be.player2 != null) Szar.chessActivePlayers.remove(be.player2); + } + super.onBreak(world, pos, state, player); + } } diff --git a/src/main/java/dev/tggamesyt/szar/ChessBlockEntity.java b/src/main/java/dev/tggamesyt/szar/ChessBlockEntity.java index b54ee5e..69ccc84 100644 --- a/src/main/java/dev/tggamesyt/szar/ChessBlockEntity.java +++ b/src/main/java/dev/tggamesyt/szar/ChessBlockEntity.java @@ -1,4 +1,308 @@ package dev.tggamesyt.szar; -public class ChessBlockEntity { -} +import com.github.bhlangonijr.chesslib.Board; +import com.github.bhlangonijr.chesslib.Side; +import com.github.bhlangonijr.chesslib.move.Move; +import com.github.bhlangonijr.chesslib.Square; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class ChessBlockEntity extends BlockEntity { + public String DEFAULT_POSITION = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + // FEN string stores full board state — chesslib handles all logic + public String fen = DEFAULT_POSITION; + public UUID player1 = null; // white + public UUID player2 = null; // black + public int winner = 0; // 0=ongoing, 1=white, 2=black, 3=draw + public String statusMessage = ""; + public Set spectators = new HashSet<>(); + public int resetTimer = -1; + + public ChessBlockEntity(BlockPos pos, BlockState state) { + super(Szar.CHESS_ENTITY, pos, state); + } + + public static void tick(World world, BlockPos pos, BlockState state, + ChessBlockEntity entity) { + if (!world.isClient && entity.resetTimer > 0) { + entity.resetTimer--; + if (entity.resetTimer == 0) { + entity.resetTimer = -1; + var server = ((net.minecraft.server.world.ServerWorld) world).getServer(); + entity.resetGame(server); + entity.syncToPlayers(server); + } + } + } + + public void handlePlayerJoin(ServerPlayerEntity player, BlockPos pos) { + UUID uuid = player.getUuid(); + + BlockPos activePos = Szar.chessActivePlayers.get(uuid); + if (activePos != null && !activePos.equals(pos)) { + player.sendMessage(Text.literal("§cYou are already in a chess game elsewhere!"), true); + return; + } + + if (uuid.equals(player1) || uuid.equals(player2)) { + if (player1 != null && player2 != null) { + openScreen(player); + } else { + // Leave if waiting + if (uuid.equals(player1)) { + Szar.chessActivePlayers.remove(player1); + player1 = null; + } else { + Szar.chessActivePlayers.remove(player2); + player2 = null; + } + player.sendMessage(Text.literal("§7Left the game."), true); + markDirty(); + } + return; + } + + if (player1 == null) { + player1 = uuid; + Szar.chessActivePlayers.put(uuid, pos); + player.sendMessage(Text.literal("§aYou are §fWhite§a! Waiting for second player..."), true); + markDirty(); + return; + } + + if (player2 == null && !uuid.equals(player1)) { + player2 = uuid; + Szar.chessActivePlayers.put(uuid, pos); + player.sendMessage(Text.literal("§aYou are §8Black§a! Game starting!"), true); + ServerPlayerEntity p1 = player.getServer().getPlayerManager().getPlayer(player1); + if (p1 != null) p1.sendMessage(Text.literal("§aSecond player joined! Your turn (White)!"), true); + openScreenForBoth(player); + markDirty(); + return; + } + + if (player1 != null && player2 != null) { + spectators.add(uuid); + player.sendMessage(Text.literal("§7Spectating the match..."), true); + openScreen(player); + markDirty(); + } + } + + public void handleMove(ServerPlayerEntity player, String uciMove) { + if (winner != 0) return; + + UUID uuid = player.getUuid(); + Board board = new Board(); + board.loadFromFen(fen); + + // Check it's the right player's turn + Side sideToMove = board.getSideToMove(); + boolean isWhite = uuid.equals(player1); + boolean isBlack = uuid.equals(player2); + if (!isWhite && !isBlack) return; + if (sideToMove == Side.WHITE && !isWhite) { + player.sendMessage(Text.literal("§cNot your turn!"), true); + return; + } + if (sideToMove == Side.BLACK && !isBlack) { + player.sendMessage(Text.literal("§cNot your turn!"), true); + return; + } + + // Validate and apply move + try { + Square from = Square.valueOf(uciMove.substring(0, 2).toUpperCase()); + Square to = Square.valueOf(uciMove.substring(2, 4).toUpperCase()); + + // Handle promotion — default to queen + String promotion = uciMove.length() > 4 ? uciMove.substring(4) : ""; + Move move; + if (!promotion.isEmpty()) { + com.github.bhlangonijr.chesslib.Piece promoPiece = + sideToMove == Side.WHITE + ? com.github.bhlangonijr.chesslib.Piece.WHITE_QUEEN + : com.github.bhlangonijr.chesslib.Piece.BLACK_QUEEN; + move = new Move(from, to, promoPiece); + } else { + move = new Move(from, to); + } + + // Check move is legal + if (!board.legalMoves().contains(move)) { + player.sendMessage(Text.literal("§cIllegal move!"), true); + return; + } + + board.doMove(move); + fen = board.getFen(); + + // Check game end conditions + if (board.isMated()) { + winner = sideToMove == Side.WHITE ? 1 : 2; + statusMessage = (sideToMove == Side.WHITE ? "White" : "Black") + " wins by checkmate!"; + resetTimer = 100; + } else if (board.isStaleMate()) { + winner = 3; + statusMessage = "Draw by stalemate!"; + resetTimer = 100; + } else if (board.isInsufficientMaterial()) { + winner = 3; + statusMessage = "Draw by insufficient material!"; + resetTimer = 100; + } else if (board.isRepetition()) { + winner = 3; + statusMessage = "Draw by repetition!"; + resetTimer = 100; + } else if (board.isKingAttacked()) { + statusMessage = "Check!"; + } else { + statusMessage = ""; + } + + markDirty(); + syncToPlayers(player.getServer()); + + } catch (Exception e) { + player.sendMessage(Text.literal("§cInvalid move format!"), true); + } + } + + public void openScreenForBoth(ServerPlayerEntity joiner) { + openScreen(joiner); + ServerPlayerEntity p1 = joiner.getServer().getPlayerManager().getPlayer(player1); + if (p1 != null) openScreen(p1); + } + + public void openScreen(ServerPlayerEntity player) { + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeBlockPos(this.pos); + writeStateToBuf(buf, player.getUuid()); + ServerPlayNetworking.send(player, Szar.CHESS_OPEN_SCREEN, buf); + } + + public void syncToPlayers(net.minecraft.server.MinecraftServer server) { + sendToPlayer(server, player1); + sendToPlayer(server, player2); + for (UUID uuid : spectators) sendToPlayer(server, uuid); + } + + private void sendToPlayer(net.minecraft.server.MinecraftServer server, UUID uuid) { + if (uuid == null) return; + ServerPlayerEntity p = server.getPlayerManager().getPlayer(uuid); + if (p == null) return; + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeBlockPos(this.pos); + writeStateToBuf(buf, uuid); + ServerPlayNetworking.send(p, Szar.CHESS_STATE_SYNC, buf); + } + + public void writeStateToBuf(PacketByteBuf buf, UUID viewerUuid) { + buf.writeString(fen); + buf.writeBoolean(player1 != null); + if (player1 != null) buf.writeUuid(player1); + buf.writeBoolean(player2 != null); + if (player2 != null) buf.writeUuid(player2); + buf.writeInt(winner); + buf.writeString(statusMessage); + boolean isSpectator = viewerUuid != null + && !viewerUuid.equals(player1) + && !viewerUuid.equals(player2); + buf.writeBoolean(isSpectator); + // Is this viewer playing white or black + boolean isWhite = viewerUuid != null && viewerUuid.equals(player1); + buf.writeBoolean(isWhite); + } + + public static State readStateFromBuf(PacketByteBuf buf) { + State s = new State(); + s.fen = buf.readString(); + if (buf.readBoolean()) s.player1 = buf.readUuid(); + if (buf.readBoolean()) s.player2 = buf.readUuid(); + s.winner = buf.readInt(); + s.statusMessage = buf.readString(); + s.isSpectator = buf.readBoolean(); + s.isWhite = buf.readBoolean(); + return s; + } + + public static class State { + public String fen; + public UUID player1, player2; + public int winner; + public String statusMessage; + public boolean isSpectator; + public boolean isWhite; // true = viewing from white's perspective + } + + public void resetGame(net.minecraft.server.MinecraftServer server) { + closeScreenForAll(server); + if (player1 != null) Szar.chessActivePlayers.remove(player1); + if (player2 != null) Szar.chessActivePlayers.remove(player2); + spectators.clear(); + fen = DEFAULT_POSITION; + player1 = null; + player2 = null; + winner = 0; + statusMessage = ""; + resetTimer = -1; + markDirty(); + } + + public void closeScreenForAll(net.minecraft.server.MinecraftServer server) { + closeScreenForPlayer(server, player1); + closeScreenForPlayer(server, player2); + for (UUID uuid : spectators) closeScreenForPlayer(server, uuid); + } + + private void closeScreenForPlayer(net.minecraft.server.MinecraftServer server, UUID uuid) { + if (uuid == null) return; + ServerPlayerEntity p = server.getPlayerManager().getPlayer(uuid); + if (p == null) return; + ServerPlayNetworking.send(p, Szar.CHESS_CLOSE_SCREEN, PacketByteBufs.empty()); + } + + @Override + public void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + nbt.putString("Fen", fen); + if (player1 != null) nbt.putUuid("Player1", player1); + if (player2 != null) nbt.putUuid("Player2", player2); + nbt.putInt("Winner", winner); + nbt.putString("Status", statusMessage); + nbt.putInt("ResetTimer", resetTimer); + } + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + fen = nbt.getString("Fen"); + if (fen.isEmpty()) fen = DEFAULT_POSITION; + if (nbt.containsUuid("Player1")) player1 = nbt.getUuid("Player1"); + if (nbt.containsUuid("Player2")) player2 = nbt.getUuid("Player2"); + winner = nbt.getInt("Winner"); + statusMessage = nbt.getString("Status"); + resetTimer = nbt.getInt("ResetTimer"); + } + + @Override + public NbtCompound toInitialChunkDataNbt() { return createNbt(); } + + @Override + public BlockEntityUpdateS2CPacket toUpdatePacket() { + return BlockEntityUpdateS2CPacket.create(this); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/Szar.java b/src/main/java/dev/tggamesyt/szar/Szar.java index 63da258..7a93d53 100644 --- a/src/main/java/dev/tggamesyt/szar/Szar.java +++ b/src/main/java/dev/tggamesyt/szar/Szar.java @@ -386,6 +386,7 @@ public class Szar implements ModInitializer { entries.add(Szar.KEBAB); entries.add(Szar.TIC_TAC_TOE_ITEM); entries.add(Szar.CONNECT_FOUR_ITEM); + entries.add(Szar.CHESS_ITEM); // crazy weponary entries.add(Szar.BULLET_ITEM); entries.add(Szar.AK47); @@ -1347,6 +1348,15 @@ public class Szar implements ModInitializer { } }); }); + ServerPlayNetworking.registerGlobalReceiver(CHESS_MAKE_MOVE, (server, player, handler, buf, sender) -> { + BlockPos pos = buf.readBlockPos(); + String move = buf.readString(); // UCI format e.g. "e2e4" + server.execute(() -> { + if (player.getWorld().getBlockEntity(pos) instanceof ChessBlockEntity be) { + be.handleMove(player, move); + } + }); + }); } public static final Block TIC_TAC_TOE_BLOCK = Registry.register( @@ -1390,6 +1400,28 @@ public class Szar implements ModInitializer { public static final Identifier C4_CLOSE_SCREEN = new Identifier(MOD_ID, "c4_close"); public static final Map c4ActivePlayers = new java.util.HashMap<>(); + + public static final Block CHESS_BLOCK = Registry.register( + Registries.BLOCK, new Identifier(MOD_ID, "chess"), + new ChessBlock(AbstractBlock.Settings.create().strength(2f)) + ); + public static final BlockItem CHESS_ITEM = Registry.register( + Registries.ITEM, new Identifier(MOD_ID, "chess"), + new BlockItem(CHESS_BLOCK, new Item.Settings()) + ); + public static final BlockEntityType CHESS_ENTITY = + Registry.register( + Registries.BLOCK_ENTITY_TYPE, new Identifier(MOD_ID, "chess"), + FabricBlockEntityTypeBuilder.create(ChessBlockEntity::new, CHESS_BLOCK).build() + ); + + public static final Identifier CHESS_OPEN_SCREEN = new Identifier(MOD_ID, "chess_open"); + public static final Identifier CHESS_MAKE_MOVE = new Identifier(MOD_ID, "chess_move"); + public static final Identifier CHESS_STATE_SYNC = new Identifier(MOD_ID, "chess_sync"); + public static final Identifier CHESS_CLOSE_SCREEN = new Identifier(MOD_ID, "chess_close"); + + public static final Map chessActivePlayers = new java.util.HashMap<>(); + // Blocks public static final TrackerBlock TRACKER_BLOCK = Registry.register( Registries.BLOCK, new Identifier(MOD_ID, "tracker"), diff --git a/src/main/resources/assets/szar/blockstates/chess.json b/src/main/resources/assets/szar/blockstates/chess.json new file mode 100644 index 0000000..e1d0393 --- /dev/null +++ b/src/main/resources/assets/szar/blockstates/chess.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "szar:block/chess" } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/szar/lang/en_us.json b/src/main/resources/assets/szar/lang/en_us.json index d9635c1..7b2f230 100644 --- a/src/main/resources/assets/szar/lang/en_us.json +++ b/src/main/resources/assets/szar/lang/en_us.json @@ -188,5 +188,6 @@ "advancement.szar.backrooms.description": "Step into the Backrooms", "block.szar.tictactoe": "Tic Tac Toe", - "block.szar.connectfour": "Connect Four" + "block.szar.connectfour": "Connect Four", + "block.szar.chess": "Chess" } diff --git a/src/main/resources/assets/szar/models/block/chess.json b/src/main/resources/assets/szar/models/block/chess.json new file mode 100644 index 0000000..5eb41c8 --- /dev/null +++ b/src/main/resources/assets/szar/models/block/chess.json @@ -0,0 +1,24 @@ +{ + "parent": "block/block", + "format_version": "1.9.0", + "credit": "Made with Blockbench", + "textures": { + "0": "szar:block/chess", + "1": "szar:block/plank", + "particle": "szar:block/chess" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 12, 16], + "faces": { + "north": {"uv": [0, 0, 16, 12], "texture": "#1"}, + "east": {"uv": [0, 0, 16, 12], "texture": "#1"}, + "south": {"uv": [0, 0, 16, 12], "texture": "#1"}, + "west": {"uv": [0, 0, 16, 12], "texture": "#1"}, + "up": {"uv": [0, 0, 16, 16], "texture": "#0"}, + "down": {"uv": [0, 0, 16, 16], "texture": "#1"} + } + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/szar/models/item/chess.json b/src/main/resources/assets/szar/models/item/chess.json new file mode 100644 index 0000000..1c28450 --- /dev/null +++ b/src/main/resources/assets/szar/models/item/chess.json @@ -0,0 +1,3 @@ +{ + "parent": "szar:block/chess" +} \ No newline at end of file diff --git a/src/main/resources/assets/szar/textures/block/chess.png b/src/main/resources/assets/szar/textures/block/chess.png new file mode 100644 index 0000000..8af07ec Binary files /dev/null and b/src/main/resources/assets/szar/textures/block/chess.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_bb.png b/src/main/resources/assets/szar/textures/gui/chess_bb.png new file mode 100644 index 0000000..9725f5e Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_bb.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_bk.png b/src/main/resources/assets/szar/textures/gui/chess_bk.png new file mode 100644 index 0000000..720cfa0 Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_bk.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_bn.png b/src/main/resources/assets/szar/textures/gui/chess_bn.png new file mode 100644 index 0000000..c534b8d Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_bn.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_bp.png b/src/main/resources/assets/szar/textures/gui/chess_bp.png new file mode 100644 index 0000000..b90295d Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_bp.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_bq.png b/src/main/resources/assets/szar/textures/gui/chess_bq.png new file mode 100644 index 0000000..b238f17 Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_bq.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_br.png b/src/main/resources/assets/szar/textures/gui/chess_br.png new file mode 100644 index 0000000..f1f6d2e Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_br.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_wb.png b/src/main/resources/assets/szar/textures/gui/chess_wb.png new file mode 100644 index 0000000..a75a9af Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_wb.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_wk.png b/src/main/resources/assets/szar/textures/gui/chess_wk.png new file mode 100644 index 0000000..8da73d0 Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_wk.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_wn.png b/src/main/resources/assets/szar/textures/gui/chess_wn.png new file mode 100644 index 0000000..5da9551 Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_wn.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_wp.png b/src/main/resources/assets/szar/textures/gui/chess_wp.png new file mode 100644 index 0000000..c34cce5 Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_wp.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_wq.png b/src/main/resources/assets/szar/textures/gui/chess_wq.png new file mode 100644 index 0000000..90f2667 Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_wq.png differ diff --git a/src/main/resources/assets/szar/textures/gui/chess_wr.png b/src/main/resources/assets/szar/textures/gui/chess_wr.png new file mode 100644 index 0000000..8d43fad Binary files /dev/null and b/src/main/resources/assets/szar/textures/gui/chess_wr.png differ diff --git a/src/main/resources/data/minecraft/tags/blocks/mineable/axe.json b/src/main/resources/data/minecraft/tags/blocks/mineable/axe.json index d5cd026..6ef030f 100644 --- a/src/main/resources/data/minecraft/tags/blocks/mineable/axe.json +++ b/src/main/resources/data/minecraft/tags/blocks/mineable/axe.json @@ -3,6 +3,7 @@ "szar:roulette", "szar:slot_machine", "szar:tictactoe", - "szar:connectfour" + "szar:connectfour", + "szar:chess" ] } \ No newline at end of file diff --git a/src/main/resources/data/szar/loot_tables/blocks/chess.json b/src/main/resources/data/szar/loot_tables/blocks/chess.json new file mode 100644 index 0000000..0cea3f7 --- /dev/null +++ b/src/main/resources/data/szar/loot_tables/blocks/chess.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "szar:chess" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/data/szar/recipes/chess.json b/src/main/resources/data/szar/recipes/chess.json new file mode 100644 index 0000000..891c3a9 --- /dev/null +++ b/src/main/resources/data/szar/recipes/chess.json @@ -0,0 +1,23 @@ +{ + "type": "minecraft:crafting_shaped", + "pattern": [ + "RBR", + "BPB", + "RBR" + ], + "key": { + "R": { + "item": "minecraft:white_dye" + }, + "B": { + "item": "minecraft:black_dye" + }, + "P": { + "tag": "minecraft:planks" + } + }, + "result": { + "item": "szar:chess", + "count": 1 + } +} \ No newline at end of file