Skip to content

Commit

Permalink
Add some more Connect Four flavour
Browse files Browse the repository at this point in the history
  • Loading branch information
Gegy committed Nov 15, 2024
1 parent c6aa575 commit a047805
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public final class MinigameTexts {
public static final TranslationCollector.Fun1 PLAYER_WON = KEYS.add1("player_won", "⭐ %s won the game!");
public static final Component NOBODY_WON = KEYS.add("nobody_won", "⭐ Nobody won the game!");
public static final Component RESULTS = KEYS.add("results", "The game is over! Here are the results:");
public static final Component GAME_OVER = KEYS.add("game_over.title", "Game Over!");
public static final Component GAME_OVER = KEYS.add("game_over.title", "Game Over!").withStyle(ChatFormatting.GOLD);

public static final TranslationCollector.Fun1 JOIN_TEAM = KEYS.add1("teams.join", "Join %s");
public static final TranslationCollector.Fun1 JOINED_TEAM = KEYS.add1("teams.joined", "You have requested to join: %s").withStyle(ChatFormatting.GRAY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.lovetropics.lib.BlockBox;
import com.lovetropics.lib.codec.MoreCodecs;
import com.lovetropics.lib.entity.FireworkPalette;
import com.lovetropics.minigames.common.content.MinigameTexts;
import com.lovetropics.minigames.common.core.game.GameException;
import com.lovetropics.minigames.common.core.game.GameStopReason;
Expand All @@ -14,7 +15,6 @@
import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents;
import com.lovetropics.minigames.common.core.game.behavior.event.GameWorldEvents;
import com.lovetropics.minigames.common.core.game.state.statistics.PlayerKey;
import com.lovetropics.minigames.common.core.game.state.statistics.StatisticKey;
import com.lovetropics.minigames.common.core.game.state.team.GameTeam;
import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey;
import com.lovetropics.minigames.common.core.game.state.team.TeamState;
Expand All @@ -23,19 +23,24 @@
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.ChatFormatting;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

Expand Down Expand Up @@ -77,7 +82,7 @@ public ConnectFourBehavior(Map<GameTeamKey, GameBlock> teamBlocks, String placin
private TeamState teams;
private BlockBox placingRegion;

private GameTeamKey[][] pieces;
private PlacedPiece[][] pieces;
private int placedPieces;

@Override
Expand All @@ -86,7 +91,7 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio
placingRegion = game.mapRegions().getOrThrow(placingRegionKey);
teams = game.instanceState().getOrThrow(TeamState.KEY);

pieces = new GameTeamKey[width][height];
pieces = new PlacedPiece[width][height];
placedPieces = 0;

events.listen(GamePhaseEvents.START, this::onStart);
Expand Down Expand Up @@ -127,9 +132,7 @@ private InteractionResult onPlaceBlock(ServerPlayer player, BlockPos pos, BlockS
return InteractionResult.PASS;
}

private void onBlockLanded(Level level, BlockPos pos, BlockState state) {
if (level.isClientSide) return;

private void onBlockLanded(ServerLevel level, BlockPos pos, BlockState state) {
var expected = teamBlocks.get(playingTeams.current().key);

if (!state.is(expected.powder)) return;
Expand All @@ -147,15 +150,12 @@ private void onBlockLanded(Level level, BlockPos pos, BlockState state) {
level.setBlock(pos.above(3), blocker, Block.UPDATE_ALL);
}

int x = (pos.getX() - Math.min(placingRegion.min().getX(), placingRegion.max().getX())) / 2;
if (placingRegion.min().getX() == placingRegion.max().getX()) {
x = (pos.getZ() - Math.min(placingRegion.min().getZ(), placingRegion.max().getZ())) / 2;
}
int x = blockToGridX(pos);
var column = pieces[x];
int y;
for (y = 0; y < column.length; y++) {
if (column[y] == null) {
column[y] = playingTeams.current().key();
column[y] = new PlacedPiece(pos, playingTeams.current().key());
break;
}
}
Expand All @@ -165,26 +165,73 @@ private void onBlockLanded(Level level, BlockPos pos, BlockState state) {

game.allPlayers().getPlayerBy(playingTeams.current().players().current()).setGlowingTag(false);

if (checkWin(x, y, team)) {
Line winningLine = checkWin(x, y, team);
if (winningLine != null) {
showBlinkingLine(level, team, winningLine);
GameTeam gameTeam = teams.getTeamByKey(team);
game.invoker(GameLogicEvents.GAME_OVER).onGameWonBy(gameTeam);

game.allPlayers().forEach(ServerPlayer::closeContainer);

game.scheduler().runAfterSeconds(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(gameTeam.config().styledName()).withStyle(ChatFormatting.GREEN), true));
game.scheduler().runAfterSeconds(5, () -> game.requestStop(GameStopReason.finished()));
triggerGameOver(new GameWinner.Team(gameTeam));
} else {
if (placedPieces == width * height) {
game.invoker(GameLogicEvents.GAME_OVER).onGameOver(new GameWinner.Nobody());

game.scheduler().runAfterSeconds(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.NOBODY_WON, true));
game.scheduler().runAfterSeconds(5, () -> game.requestStop(GameStopReason.finished()));
triggerGameOver(new GameWinner.Nobody());
} else {
nextPlayer();
}
}
}

private int blockToGridX(BlockPos pos) {
if (placingRegion.min().getX() == placingRegion.max().getX()) {
return (pos.getZ() - Math.min(placingRegion.min().getZ(), placingRegion.max().getZ())) / 2;
} else {
return (pos.getX() - Math.min(placingRegion.min().getX(), placingRegion.max().getX())) / 2;
}
}

private void showBlinkingLine(ServerLevel level, GameTeamKey team, Line winningLine) {
GameBlock teamBlocks = this.teamBlocks.get(team);
MutableBoolean blink = new MutableBoolean(true);
game.scheduler().runPeriodic(0, SharedConstants.TICKS_PER_SECOND / 2,() -> {
BlockState blockState = blink.getValue() ? teamBlocks.highlighted().defaultBlockState() : teamBlocks.solid().defaultBlockState();
fillLine(level, winningLine, blockState);
blink.setValue(!blink.getValue());
});
}

private void fillLine(ServerLevel level, Line line, BlockState blockState) {
for (int i = 0; i < connectAmount; i++) {
PlacedPiece piece = pieces[line.x(i)][line.y(i)];
if (piece != null) {
level.setBlockAndUpdate(piece.pos(), blockState);
}
}
}

private void triggerGameOver(GameWinner winner) {
game.invoker(GameLogicEvents.GAME_OVER).onGameOver(winner);

game.allPlayers().playSound(SoundEvents.END_PORTAL_SPAWN, SoundSource.PLAYERS, 0.5f, 1.0f);
game.allPlayers().showTitle(MinigameTexts.GAME_OVER, null, 10, 40, 10);

if (winner instanceof GameWinner.Team(GameTeam team)) {
for (ServerPlayer winningPlayer : teams.getPlayersForTeam(team.key())) {
applyWinningPlayerEffects(winningPlayer, team);
}
game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(team.config().styledName()).withStyle(ChatFormatting.GREEN), true);
} else {
game.allPlayers().sendMessage(MinigameTexts.NOBODY_WON, true);
}

game.scheduler().runAfterSeconds(8, () -> game.requestStop(GameStopReason.finished()));
}

private void applyWinningPlayerEffects(ServerPlayer winningPlayer, GameTeam team) {
game.scheduler().runAfterSeconds(game.random().nextFloat(), () -> {
BlockPos fireworksPos = BlockPos.containing(winningPlayer.getEyePosition()).above();
FireworkPalette.forDye(team.config().dye()).spawn(fireworksPos, game.level());
});
winningPlayer.setGlowingTag(true);
}

private void nextPlayer() {
var nextTeam = playingTeams.next();
var nextPlayer = nextTeam.players().next();
Expand All @@ -196,56 +243,62 @@ private void nextPlayer() {

player.setGlowingTag(true);
player.displayClientMessage(ConnectFourTexts.IT_IS_YOUR_TURN.copy().withStyle(ChatFormatting.GOLD), true);
player.playNotifySound(SoundEvents.ANVIL_LAND, SoundSource.PLAYERS, 1.0f, 1.0f);
}

private boolean checkWin(int x, int y, GameTeamKey team) {
if (checkLine(x, y, 0, -1, team)) { // vertical
return true;
}

@Nullable
private Line checkWin(int x, int y, GameTeamKey team) {
List<Line> possibleLines = new ArrayList<>();
possibleLines.add(new Line(x, y, 0, -1)); // vertical
for (int offset = 0; offset < connectAmount; offset++) {
if (checkLine(x - (connectAmount - 1) + offset, y, 1, 0, team)) { // horizontal
return true;
}

if (checkLine(x - (connectAmount - 1) + offset, y + (connectAmount - 1) - offset, 1, -1, team)) { // leading diagonal
return true;
}

if (checkLine(x - (connectAmount - 1) + offset, y - (connectAmount - 1) + offset, 1, 1, team)) { // trailing diagonal
return true;
possibleLines.add(new Line(x - (connectAmount - 1) + offset, y, 1, 0)); // horizontal
possibleLines.add(new Line(x - (connectAmount - 1) + offset, y + (connectAmount - 1) - offset, 1, -1)); // leading diagonal
possibleLines.add(new Line(x - (connectAmount - 1) + offset, y - (connectAmount - 1) + offset, 1, 1)); // trailing diagonal
}
for (Line line : possibleLines) {
if (checkLine(line, team)) {
return line;
}
}

return false;
return null;
}


private boolean checkLine(int xs, int ys, int dx, int dy, GameTeamKey team) {
private boolean checkLine(Line line, GameTeamKey team) {
for (int i = 0; i < connectAmount; i++) {
int x = xs + (dx * i);
int y = ys + (dy * i);

int x = line.x(i);
int y = line.y(i);
if (x < 0 || x > pieces.length - 1) {
return false;
}

if (y < 0 || y > pieces[x].length - 1) {
return false;
}

if (team != pieces[x][y]) {
PlacedPiece piece = pieces[x][y];
if (piece == null || team != piece.team) {
return false;
}
}

return true;
}

private record GameBlock(Block powder, Block solid) {
private record Line(int xs, int ys, int dx, int dy) {
public int x(int i) {
return xs + dx * i;
}

public int y(int i) {
return ys + dy * i;
}
}

private record PlacedPiece(BlockPos pos, GameTeamKey team) {
}

private record GameBlock(Block powder, Block solid, Block highlighted) {
public static final MapCodec<GameBlock> CODEC = RecordCodecBuilder.mapCodec(in -> in.group(
BuiltInRegistries.BLOCK.byNameCodec().fieldOf("powder").forGetter(GameBlock::powder),
BuiltInRegistries.BLOCK.byNameCodec().fieldOf("solid").forGetter(GameBlock::solid)
BuiltInRegistries.BLOCK.byNameCodec().fieldOf("solid").forGetter(GameBlock::solid),
BuiltInRegistries.BLOCK.byNameCodec().fieldOf("highlighted").forGetter(GameBlock::highlighted)
).apply(in, GameBlock::new));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.lovetropics.minigames.common.core.game.behavior.event;

import com.lovetropics.minigames.common.core.game.weather.WeatherEvent;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.InteractionResult;
import net.minecraft.core.BlockPos;
Expand Down Expand Up @@ -69,6 +70,6 @@ public interface SetWeather {
}

public interface BlockLanded {
void onBlockLanded(Level level, BlockPos pos, BlockState state);
void onBlockLanded(ServerLevel level, BlockPos pos, BlockState state);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.lovetropics.minigames.mixin;

import com.lovetropics.minigames.common.core.game.IGameManager;
import com.lovetropics.minigames.common.core.game.IGamePhase;
import com.lovetropics.minigames.common.core.game.behavior.event.GameWorldEvents;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.item.FallingBlockEntity;
Expand All @@ -25,9 +27,11 @@ public FallingBlockEntityMixin(EntityType<?> entityType, Level level) {

@Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/item/FallingBlockEntity;discard()V", ordinal = 1, shift = At.Shift.AFTER), method = "tick")
private void customFalling(CallbackInfo ci) {
var game = IGameManager.get().getGamePhaseAt(level(), blockPosition());
if (game != null) {
game.invoker(GameWorldEvents.BLOCK_LANDED).onBlockLanded(level(), blockPosition(), blockState);
if (level() instanceof ServerLevel serverLevel) {
IGamePhase game = IGameManager.get().getGamePhaseAt(serverLevel, blockPosition());
if (game != null) {
game.invoker(GameWorldEvents.BLOCK_LANDED).onBlockLanded(serverLevel, blockPosition(), blockState);
}
}
}
}

0 comments on commit a047805

Please sign in to comment.