From 49f583e4e86661f492e9d4c9f724546beb5c5132 Mon Sep 17 00:00:00 2001 From: Cory Scheviak Date: Sun, 6 Oct 2024 12:32:20 +0200 Subject: [PATCH 01/15] Add a configurable game queue for microgames and go from microgame to microgame before going back to the main game --- .../behaviour/MicrogamesBehaviour.java | 33 +++++++++--- .../common/core/game/impl/MultiGamePhase.java | 50 +++++++++++++------ 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/MicrogamesBehaviour.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/MicrogamesBehaviour.java index a81d8c79..30779d53 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/MicrogamesBehaviour.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/MicrogamesBehaviour.java @@ -8,9 +8,9 @@ import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; import com.lovetropics.minigames.common.core.game.config.GameConfig; import com.lovetropics.minigames.common.core.game.config.GameConfigs; -import com.lovetropics.minigames.common.core.game.impl.GameInstance; import com.lovetropics.minigames.common.core.game.impl.GamePhase; import com.lovetropics.minigames.common.core.game.impl.MultiGamePhase; +import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.network.chat.PlayerChatMessage; @@ -18,27 +18,48 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.ExtraCodecs; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -public record MicrogamesBehaviour(List gameConfigs) implements IGameBehavior { +public record MicrogamesBehaviour(List gameConfigs, int gamesPerRound) implements IGameBehavior { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( - ExtraCodecs.nonEmptyList(ResourceLocation.CODEC.listOf()).fieldOf("games").forGetter(MicrogamesBehaviour::gameConfigs) + ExtraCodecs.nonEmptyList(ResourceLocation.CODEC.listOf()).fieldOf("games").forGetter(MicrogamesBehaviour::gameConfigs), + Codec.INT.optionalFieldOf("games_per_round", 1).forGetter(c -> c.gamesPerRound) ).apply(i, MicrogamesBehaviour::new)); @Override public void register(IGamePhase game, EventRegistrar events) throws GameException { events.listen(GamePlayerEvents.CHAT, (ServerPlayer player, PlayerChatMessage message) -> { - startMicrogame(game, "lt:block_party"); + queueMicrogames(game); + //startMicrogame(game); + startQueuedMicrogame(game); return false; }); } + public void queueMicrogames(IGamePhase game) { + if (game instanceof MultiGamePhase multiGamePhase) { + multiGamePhase.clearQueuedGames(); + + final List configs = new ArrayList<>(gameConfigs); + Collections.shuffle(configs); + multiGamePhase.queueGames(configs.subList(0, gamesPerRound)); + } + } + + public void startQueuedMicrogame(final IGamePhase game) { + if (game instanceof MultiGamePhase multiGamePhase) { + multiGamePhase.startNextQueuedMicrogame(true); + } + } + public void startMicrogame(IGamePhase game, String gameName){ - if(game instanceof MultiGamePhase multiGamePhase){ + if (game instanceof MultiGamePhase multiGamePhase) { GameConfig gameConfig = GameConfigs.REGISTRY.get(ResourceLocation.parse(gameName)); GamePhase.create(multiGamePhase.game(), gameConfig.getPlayingPhase(), GamePhaseType.PLAYING).thenAccept((result) -> { - multiGamePhase.addAndSetActivePhase(result.getOk()); + multiGamePhase.setActivePhase(result.getOk(), true); }); } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java index c0f16d5d..857b326c 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java @@ -4,18 +4,20 @@ import com.lovetropics.minigames.common.core.game.GamePhaseType; import com.lovetropics.minigames.common.core.game.GameResult; import com.lovetropics.minigames.common.core.game.GameStopReason; -import com.lovetropics.minigames.common.core.game.IGamePhase; import com.lovetropics.minigames.common.core.game.IGamePhaseDefinition; import com.lovetropics.minigames.common.core.game.PlayerIsolation; import com.lovetropics.minigames.common.core.game.behavior.BehaviorList; import com.lovetropics.minigames.common.core.game.behavior.event.GameEventType; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.lovetropics.minigames.common.core.game.config.GameConfig; +import com.lovetropics.minigames.common.core.game.config.GameConfigs; import com.lovetropics.minigames.common.core.game.map.GameMap; import com.lovetropics.minigames.common.core.game.player.PlayerRole; import com.lovetropics.minigames.common.core.game.rewards.GameRewardsMap; import com.lovetropics.minigames.common.core.game.state.GameStateMap; import com.lovetropics.minigames.common.core.map.MapRegions; import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Unit; @@ -23,30 +25,23 @@ import javax.annotation.Nullable; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; public class MultiGamePhase extends GamePhase { @Nullable private GamePhase activePhase = null; - private final Set subPhases = new HashSet<>(); + private final List subPhaseGames = Lists.newArrayList(); protected MultiGamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors) { super(game, definition, phaseType, map, behaviors); } - public void addAndSetActivePhase(GamePhase activePhase){ - subPhases.add(activePhase); - setActivePhase(activePhase); - } - - public void setActivePhase(GamePhase activePhase){ + public void setActivePhase(GamePhase activePhase, final boolean saveInventory) { this.activePhase = activePhase; MultiGameManager.INSTANCE.addGamePhaseToDimension(activePhase.dimension(), activePhase); activePhase.state().register(GameRewardsMap.STATE, ((GameLobby) activePhase.lobby()).getRewardsMap()); - activePhase.start(true); + activePhase.start(saveInventory); } public void returnHere(){ @@ -70,8 +65,8 @@ private void returnPlayerToParentPhase(ServerPlayer player, @Nullable PlayerRole addedPlayers.add(player.getUUID()); } - public Set getSubPhases() { - return subPhases; + public List getSubPhaseGames() { + return subPhaseGames; } @Override @@ -102,11 +97,13 @@ GameStopReason tick() { // Also tick our current sub-game phase if(activePhase != null){ if(activePhase.tick() != null){ - subPhases.remove(activePhase); MultiGameManager.INSTANCE.removeGamePhaseFromDimension(activePhase.dimension(), activePhase); activePhase.destroy(); activePhase = null; - returnHere(); + + if (!startNextQueuedMicrogame(false)) { + returnHere(); + } } } else { return super.tick(); @@ -159,4 +156,27 @@ void destroy() { } super.destroy(); } + + public void clearQueuedGames() { + subPhaseGames.clear(); + } + + public void queueGames(List games) { + subPhaseGames.addAll(games); + } + + public boolean startNextQueuedMicrogame(final boolean saveInventory) { + // No queued games left + if (subPhaseGames.isEmpty()) { + return false; + } + + final ResourceLocation gameKey = subPhaseGames.removeFirst(); + GameConfig gameConfig = GameConfigs.REGISTRY.get(gameKey); + GamePhase.create(game(), gameConfig.getPlayingPhase(), GamePhaseType.PLAYING).thenAccept((result) -> { + setActivePhase(result.getOk(), saveInventory); + }); + + return true; + } } From c5096a05bf6e9212723610b08aba0e01528a9f76 Mon Sep 17 00:00:00 2001 From: Cory Scheviak Date: Mon, 7 Oct 2024 21:16:35 +0200 Subject: [PATCH 02/15] Reward victory points for winning microgames --- .../block_party/BlockPartyBehavior.java | 3 ++ .../behaviour/VictoryPointsBehavior.java | 43 +++++++++++++------ .../river_race/event/RiverRaceEvents.java | 11 +++++ .../common/core/game/impl/GameLobby.java | 14 ++++++ .../common/core/game/impl/MultiGamePhase.java | 7 ++- 5 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java index 9e6fe9b1..7e8e7eaa 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/block_party/BlockPartyBehavior.java @@ -9,6 +9,7 @@ import com.lovetropics.minigames.common.core.game.SpawnBuilder; import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; import com.lovetropics.minigames.common.core.game.player.PlayerRole; @@ -171,6 +172,8 @@ private void tick() { if (winningPlayer != null) { message = MinigameTexts.PLAYER_WON.apply(winningPlayer.getDisplayName()).withStyle(ChatFormatting.GREEN); game.statistics().global().set(StatisticKey.WINNING_PLAYER, PlayerKey.from(winningPlayer)); + + game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(winningPlayer.getDisplayName()); } else { message = MinigameTexts.NOBODY_WON.copy().withStyle(ChatFormatting.RED); } diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java index a79d85a7..890a1167 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java @@ -8,7 +8,6 @@ import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; -import com.lovetropics.minigames.common.core.game.state.GameStateMap; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -19,6 +18,9 @@ import net.minecraft.world.InteractionResult; import net.minecraft.world.level.block.state.BlockState; +import javax.annotation.Nullable; +import java.util.Objects; + public class VictoryPointsBehavior implements IGameBehavior { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( @@ -29,8 +31,6 @@ public class VictoryPointsBehavior implements IGameBehavior { private IGamePhase game; - private VictoryPointsState points; - private final int pointsPerQuestion; private final int pointsPerBlockCollected; private final int pointsPerGameWon; @@ -50,17 +50,36 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio // Victory points from collectible blocks events.listen(GamePlayerEvents.BREAK_BLOCK, this::onBlockBroken); // Victory points from winning microgame - // TODO is this the right event to use here? - events.listen(GameLogicEvents.GAME_OVER, this::onGameOver); + events.listen(GameLogicEvents.WIN_TRIGGERED, this::onWinTriggered); } - @Override - public void registerState(IGamePhase game, GameStateMap phaseState, GameStateMap instanceState) { - points = phaseState.register(VictoryPointsState.KEY, new VictoryPointsState(game)); + private void onWinTriggered(Component component) { + for (final ServerPlayer player : game.participants()) { + if (Objects.equals(player.getDisplayName(), component)) { + tryAddPoints(player, pointsPerGameWon); + player.displayClientMessage(Component.literal("YOU WIN!!!! Victory points for team: " + getPoints(player)), false); + } + } } - private void onGameOver() { + private void tryAddPoints(final ServerPlayer player, final int points) { + final VictoryPointsState pointState = points(); + if (pointState != null) { + pointState.addPointsToTeam(player, points); + } + } + + private int getPoints(final ServerPlayer player) { + final VictoryPointsState pointState = points(); + if (pointState != null) { + return pointState.getPoints(player); + } + return -1; + } + @Nullable + private VictoryPointsState points() { + return game.state().getOrNull(VictoryPointsState.KEY); } private InteractionResult onBlockBroken(ServerPlayer serverPlayer, BlockPos blockPos, BlockState blockState, InteractionHand interactionHand) { @@ -69,10 +88,10 @@ private InteractionResult onBlockBroken(ServerPlayer serverPlayer, BlockPos bloc private void onQuestionAnswered(ServerPlayer player, boolean correct) { if (correct) { - points.addPointsToTeam(player, pointsPerQuestion); - player.displayClientMessage(Component.literal("CORRECT! Victory points for team: " + points.getPoints(player)), false); + tryAddPoints(player, pointsPerQuestion); + player.displayClientMessage(Component.literal("CORRECT! Victory points for team: " + getPoints(player)), false); } else { - player.displayClientMessage(Component.literal("WRONG >:( Victory points for team: " + points.getPoints(player)), false); + player.displayClientMessage(Component.literal("WRONG >:( Victory points for team: " + getPoints(player)), false); } } } diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/event/RiverRaceEvents.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/event/RiverRaceEvents.java index d902cc28..d572fa51 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/event/RiverRaceEvents.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/event/RiverRaceEvents.java @@ -1,6 +1,7 @@ package com.lovetropics.minigames.common.content.river_race.event; import com.lovetropics.minigames.common.core.game.behavior.event.GameEventType; +import com.lovetropics.minigames.common.core.game.impl.MultiGamePhase; import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; import net.minecraft.server.level.ServerPlayer; @@ -18,6 +19,12 @@ public class RiverRaceEvents { } }); + public static final GameEventType MICROGAME_STARTED = GameEventType.create(MicrogameStarted.class, listeners -> (game) -> { + for (MicrogameStarted listener : listeners) { + listener.onMicrogameStarted(game); + } + }); + public interface AnswerTriviaQuestion { void onAnswer(ServerPlayer player, final boolean correct); } @@ -25,4 +32,8 @@ public interface AnswerTriviaQuestion { public interface VictoryPointsChanged { void onVictoryPointsChanged(GameTeamKey team, int value, int lastValue); } + + public interface MicrogameStarted { + void onMicrogameStarted(MultiGamePhase game); + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java index 087f76bc..780e20b1 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java @@ -6,6 +6,7 @@ import com.lovetropics.minigames.client.lobby.state.message.LeftLobbyMessage; import com.lovetropics.minigames.client.lobby.state.message.LobbyPlayersMessage; import com.lovetropics.minigames.client.lobby.state.message.LobbyUpdateMessage; +import com.lovetropics.minigames.common.content.river_race.state.VictoryPointsState; import com.lovetropics.minigames.common.core.game.GameResult; import com.lovetropics.minigames.common.core.game.IGame; import com.lovetropics.minigames.common.core.game.IGameDefinition; @@ -53,6 +54,7 @@ final class GameLobby implements IGameLobby { new ChatNotifyListener() ); private final GameRewardsMap rewardsMap = new GameRewardsMap(); + @Nullable private VictoryPointsState points; private boolean needsRolePrompt = false; private boolean closed; @@ -165,6 +167,15 @@ public GameRewardsMap getRewardsMap() { return rewardsMap; } + public VictoryPointsState createOrGetPoints(final MultiGamePhase gamePhase) { + if (points != null) { + return points; + } + + points = new VictoryPointsState(gamePhase); + return points; + } + // If old phase is null, it probably means we're entering from the main event world private GameResult onGamePhaseChange(@Nullable GamePhase oldPhase, @Nullable GamePhase newPhase) { GameResult result = GameResult.ok(); @@ -196,6 +207,9 @@ private GameResult onGamePhaseChange(@Nullable GamePhase oldPhase, @Nullab private GameResult startPhase(GamePhase phase) { phase.state().register(GameRewardsMap.STATE, rewardsMap); + if (phase instanceof final MultiGamePhase multiPhase) { + phase.state().register(VictoryPointsState.KEY, createOrGetPoints(multiPhase)); + } return phase.start(false); } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java index 857b326c..d01aa41e 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java @@ -1,6 +1,8 @@ package com.lovetropics.minigames.common.core.game.impl; import com.google.common.collect.Lists; +import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; +import com.lovetropics.minigames.common.content.river_race.state.VictoryPointsState; import com.lovetropics.minigames.common.core.game.GamePhaseType; import com.lovetropics.minigames.common.core.game.GameResult; import com.lovetropics.minigames.common.core.game.GameStopReason; @@ -40,7 +42,8 @@ protected MultiGamePhase(GameInstance game, IGamePhaseDefinition definition, Gam public void setActivePhase(GamePhase activePhase, final boolean saveInventory) { this.activePhase = activePhase; MultiGameManager.INSTANCE.addGamePhaseToDimension(activePhase.dimension(), activePhase); - activePhase.state().register(GameRewardsMap.STATE, ((GameLobby) activePhase.lobby()).getRewardsMap()); + activePhase.state().register(GameRewardsMap.STATE, ((GameLobby) lobby()).getRewardsMap()); + activePhase.state().register(VictoryPointsState.KEY, ((GameLobby) lobby()).createOrGetPoints(this)); activePhase.start(saveInventory); } @@ -177,6 +180,8 @@ public boolean startNextQueuedMicrogame(final boolean saveInventory) { setActivePhase(result.getOk(), saveInventory); }); + invoker(RiverRaceEvents.MICROGAME_STARTED).onMicrogameStarted(this); + return true; } } From a06e401d7490600f969c0d1ca4d55c02c9fe898e Mon Sep 17 00:00:00 2001 From: Cory Scheviak Date: Mon, 7 Oct 2024 21:19:49 +0200 Subject: [PATCH 03/15] Reset victory points if we try to play a game with victory points twice in the same lobby --- .../common/content/river_race/state/VictoryPointsState.java | 4 ++++ .../minigames/common/core/game/impl/GameLobby.java | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java index f08d449f..c79c15fc 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java @@ -46,4 +46,8 @@ public int getPoints(GameTeamKey team) { public int getPoints(ServerPlayer player) { return getPoints(getTeamForPlayer(player)); } + + public void reset() { + pointsTracker.clear(); + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java index 780e20b1..708cd971 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java @@ -208,6 +208,9 @@ private GameResult onGamePhaseChange(@Nullable GamePhase oldPhase, @Nullab private GameResult startPhase(GamePhase phase) { phase.state().register(GameRewardsMap.STATE, rewardsMap); if (phase instanceof final MultiGamePhase multiPhase) { + if (points != null) { + points.reset(); + } phase.state().register(VictoryPointsState.KEY, createOrGetPoints(multiPhase)); } return phase.start(false); From ce999a2613ff8f2b3022c4f00c7eb6d723ba6b43 Mon Sep 17 00:00:00 2001 From: Cory Scheviak Date: Mon, 7 Oct 2024 22:40:27 +0200 Subject: [PATCH 04/15] Make it more generic so we can store multiple things per-team in the river race game, and make multigamephase a bit more generic for storing game states in general. p hacky thooo --- .../behaviour/VictoryPointsBehavior.java | 16 ++-- .../river_race/state/RiverRaceState.java | 91 +++++++++++++++++++ .../state/VictoryPointsGameState.java | 13 +++ .../river_race/state/VictoryPointsState.java | 53 ----------- .../common/core/game/impl/GameLobby.java | 31 ++++--- .../common/core/game/impl/MultiGamePhase.java | 38 +++++++- 6 files changed, 168 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/lovetropics/minigames/common/content/river_race/state/RiverRaceState.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsGameState.java delete mode 100644 src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java index 890a1167..7a5cf952 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java @@ -1,7 +1,8 @@ package com.lovetropics.minigames.common.content.river_race.behaviour; import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; -import com.lovetropics.minigames.common.content.river_race.state.VictoryPointsState; +import com.lovetropics.minigames.common.content.river_race.state.RiverRaceState; +import com.lovetropics.minigames.common.content.river_race.state.VictoryPointsGameState; import com.lovetropics.minigames.common.core.game.GameException; import com.lovetropics.minigames.common.core.game.IGamePhase; import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; @@ -63,23 +64,24 @@ private void onWinTriggered(Component component) { } private void tryAddPoints(final ServerPlayer player, final int points) { - final VictoryPointsState pointState = points(); + final VictoryPointsGameState pointState = state(); if (pointState != null) { pointState.addPointsToTeam(player, points); } } private int getPoints(final ServerPlayer player) { - final VictoryPointsState pointState = points(); - if (pointState != null) { - return pointState.getPoints(player); + final VictoryPointsGameState gameState = state(); + if (gameState != null) { + return gameState.getVictoryPoints(player); } return -1; } @Nullable - private VictoryPointsState points() { - return game.state().getOrNull(VictoryPointsState.KEY); + private VictoryPointsGameState state() { + // TODO how to make this more generic to not be specific to river race? + return game.state().getOrNull(RiverRaceState.KEY); } private InteractionResult onBlockBroken(ServerPlayer serverPlayer, BlockPos blockPos, BlockState blockState, InteractionHand interactionHand) { diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/RiverRaceState.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/RiverRaceState.java new file mode 100644 index 00000000..2290d66a --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/RiverRaceState.java @@ -0,0 +1,91 @@ +package com.lovetropics.minigames.common.content.river_race.state; + +import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.state.GameStateKey; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import com.lovetropics.minigames.common.core.game.state.team.TeamState; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; + +public class RiverRaceState implements VictoryPointsGameState { + public static final GameStateKey KEY = GameStateKey.create("RiverRace"); + + private final IGamePhase game; + + public RiverRaceState(IGamePhase game) { + this.game = game; + } + + @Override + public void addPointsToTeam(final GameTeamKey team, final int points) { + addToTeam(Trackers.POINTS, team, points); + } + + public void addCoinsToTeam(final GameTeamKey team, final int coins) { + addToTeam(Trackers.COINS, team, coins); + } + + @Override + public void addPointsToTeam(final ServerPlayer player, final int points) { + addPointsToTeam(getTeamForPlayer(player), points); + } + + @Override + public int getVictoryPoints(GameTeamKey team) { + return Trackers.POINTS.getTracker().getOrDefault(team, 0); + } + + public int getCoins(GameTeamKey team) { + return Trackers.COINS.getTracker().getOrDefault(team, 0); + } + + @Override + public int getVictoryPoints(ServerPlayer player) { + return getVictoryPoints(getTeamForPlayer(player)); + } + + public int getCoins(ServerPlayer player) { + return getCoins(getTeamForPlayer(player)); + } + + @Override + public void reset() { + for (final Trackers tracker : Trackers.values()) { + tracker.reset(); + } + } + + @Nullable + private GameTeamKey getTeamForPlayer(ServerPlayer player) { + TeamState teams = game.instanceState().getOrThrow(TeamState.KEY); + return teams.getTeamForPlayer(player); + } + + private void addToTeam(final Trackers type, final GameTeamKey team, final int amount) { + int lastValue = type.getTracker().addTo(team, amount); + game.invoker(RiverRaceEvents.VICTORY_POINTS_CHANGED).onVictoryPointsChanged(team, lastValue + amount, lastValue); + } + + public enum Trackers { + POINTS, + COINS; + + private final Object2IntOpenHashMap tracker; + + Trackers() { + tracker = new Object2IntOpenHashMap<>(); + tracker.defaultReturnValue(0); + } + + private Object2IntOpenHashMap getTracker() { + return tracker; + } + + private void reset() { + tracker.clear(); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsGameState.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsGameState.java new file mode 100644 index 00000000..7bcf95e9 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsGameState.java @@ -0,0 +1,13 @@ +package com.lovetropics.minigames.common.content.river_race.state; + +import com.lovetropics.minigames.common.core.game.state.IGameState; +import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; +import net.minecraft.server.level.ServerPlayer; + +public interface VictoryPointsGameState extends IGameState { + void addPointsToTeam(final GameTeamKey team, final int points); + void addPointsToTeam(final ServerPlayer player, final int points); + int getVictoryPoints(GameTeamKey team); + int getVictoryPoints(ServerPlayer player); + void reset(); +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java deleted file mode 100644 index c79c15fc..00000000 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/state/VictoryPointsState.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.lovetropics.minigames.common.content.river_race.state; - -import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; -import com.lovetropics.minigames.common.core.game.IGamePhase; -import com.lovetropics.minigames.common.core.game.state.GameStateKey; -import com.lovetropics.minigames.common.core.game.state.IGameState; -import com.lovetropics.minigames.common.core.game.state.team.GameTeamKey; -import com.lovetropics.minigames.common.core.game.state.team.TeamState; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import net.minecraft.server.level.ServerPlayer; - -import javax.annotation.Nullable; - -public class VictoryPointsState implements IGameState { - public static final GameStateKey KEY = GameStateKey.create("VictoryPoints"); - - private final IGamePhase game; - - private final Object2IntOpenHashMap pointsTracker = new Object2IntOpenHashMap<>(); - - public VictoryPointsState(IGamePhase game) { - this.game = game; - - pointsTracker.defaultReturnValue(0); - } - - public void addPointsToTeam(final ServerPlayer player, final int points) { - addPointsToTeam(getTeamForPlayer(player), points); - } - - @Nullable - private GameTeamKey getTeamForPlayer(ServerPlayer player) { - TeamState teams = game.instanceState().getOrThrow(TeamState.KEY); - return teams.getTeamForPlayer(player); - } - - public void addPointsToTeam(final GameTeamKey team, final int points) { - int lastValue = pointsTracker.addTo(team, points); - game.invoker(RiverRaceEvents.VICTORY_POINTS_CHANGED).onVictoryPointsChanged(team, lastValue + points, lastValue); - } - - public int getPoints(GameTeamKey team) { - return pointsTracker.getOrDefault(team, 0); - } - - public int getPoints(ServerPlayer player) { - return getPoints(getTeamForPlayer(player)); - } - - public void reset() { - pointsTracker.clear(); - } -} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java index 708cd971..5e660570 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java @@ -1,12 +1,12 @@ package com.lovetropics.minigames.common.core.game.impl; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.lovetropics.minigames.client.lobby.state.ClientCurrentGame; import com.lovetropics.minigames.client.lobby.state.message.JoinedLobbyMessage; import com.lovetropics.minigames.client.lobby.state.message.LeftLobbyMessage; import com.lovetropics.minigames.client.lobby.state.message.LobbyPlayersMessage; import com.lovetropics.minigames.client.lobby.state.message.LobbyUpdateMessage; -import com.lovetropics.minigames.common.content.river_race.state.VictoryPointsState; import com.lovetropics.minigames.common.core.game.GameResult; import com.lovetropics.minigames.common.core.game.IGame; import com.lovetropics.minigames.common.core.game.IGameDefinition; @@ -21,15 +21,18 @@ import com.lovetropics.minigames.common.core.game.player.PlayerIterable; import com.lovetropics.minigames.common.core.game.player.PlayerRoleSelections; import com.lovetropics.minigames.common.core.game.rewards.GameRewardsMap; +import com.lovetropics.minigames.common.core.game.state.IGameState; import com.lovetropics.minigames.common.core.game.util.GameTexts; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Unit; import net.neoforged.neoforge.network.PacketDistributor; import javax.annotation.Nullable; +import java.util.Map; // TODO: do we want a different game lobby implementation for something like carnival games? /** @@ -54,7 +57,10 @@ final class GameLobby implements IGameLobby { new ChatNotifyListener() ); private final GameRewardsMap rewardsMap = new GameRewardsMap(); - @Nullable private VictoryPointsState points; + + // Game ID -> state + // Useful for things that should retain state no matter how deep into microgames you go + private final Map multiPhaseDataMap = Maps.newHashMap(); private boolean needsRolePrompt = false; private boolean closed; @@ -167,13 +173,16 @@ public GameRewardsMap getRewardsMap() { return rewardsMap; } - public VictoryPointsState createOrGetPoints(final MultiGamePhase gamePhase) { - if (points != null) { - return points; - } + public Map getMultiPhaseDataMap() { + return multiPhaseDataMap; + } - points = new VictoryPointsState(gamePhase); - return points; + public IGameState createOrGetMultiPhaseState(final MultiGamePhase gamePhase) { + final ResourceLocation gameID = gamePhase.game.definition().getId(); + if (!multiPhaseDataMap.containsKey(gameID)) { + gamePhase.registerState(this); + } + return multiPhaseDataMap.get(gameID); } // If old phase is null, it probably means we're entering from the main event world @@ -208,10 +217,8 @@ private GameResult onGamePhaseChange(@Nullable GamePhase oldPhase, @Nullab private GameResult startPhase(GamePhase phase) { phase.state().register(GameRewardsMap.STATE, rewardsMap); if (phase instanceof final MultiGamePhase multiPhase) { - if (points != null) { - points.reset(); - } - phase.state().register(VictoryPointsState.KEY, createOrGetPoints(multiPhase)); + multiPhaseDataMap.clear(); + multiPhase.registerState(this); } return phase.start(false); } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java index d01aa41e..f311f73c 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java @@ -1,8 +1,9 @@ package com.lovetropics.minigames.common.core.game.impl; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; -import com.lovetropics.minigames.common.content.river_race.state.VictoryPointsState; +import com.lovetropics.minigames.common.content.river_race.state.RiverRaceState; import com.lovetropics.minigames.common.core.game.GamePhaseType; import com.lovetropics.minigames.common.core.game.GameResult; import com.lovetropics.minigames.common.core.game.GameStopReason; @@ -17,6 +18,7 @@ import com.lovetropics.minigames.common.core.game.player.PlayerRole; import com.lovetropics.minigames.common.core.game.rewards.GameRewardsMap; import com.lovetropics.minigames.common.core.game.state.GameStateMap; +import com.lovetropics.minigames.common.core.game.state.IGameState; import com.lovetropics.minigames.common.core.map.MapRegions; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -28,22 +30,54 @@ import javax.annotation.Nullable; import java.util.Collections; import java.util.List; +import java.util.Map; public class MultiGamePhase extends GamePhase { + @FunctionalInterface + interface GameStateRegistration { + void registerState(L lobby, M phase, R id); + } + + private enum MultiPhaseGameStates { + RIVER_RACE((lobby, phase, id) -> { + IGameState state = lobby.getMultiPhaseDataMap().get(id); + if (state instanceof RiverRaceState riverRaceState) { + riverRaceState.reset(); + } else { + lobby.getMultiPhaseDataMap().put(id, new RiverRaceState(phase)); + } + phase.phaseState.register(RiverRaceState.KEY, (RiverRaceState) lobby.getMultiPhaseDataMap().get(id)); + }); + + final GameStateRegistration registration; + + MultiPhaseGameStates(GameStateRegistration registration) { + this.registration = registration; + } + } + @Nullable private GamePhase activePhase = null; private final List subPhaseGames = Lists.newArrayList(); + private static final Map gameStateMap = Maps.newHashMap(); protected MultiGamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors) { super(game, definition, phaseType, map, behaviors); + + gameStateMap.put(ResourceLocation.parse("lt:river_race"), MultiPhaseGameStates.RIVER_RACE); + } + + public void registerState(final GameLobby lobby) { + final ResourceLocation id = game.definition.getId(); + gameStateMap.get(id).registration.registerState(lobby, this, id); } public void setActivePhase(GamePhase activePhase, final boolean saveInventory) { this.activePhase = activePhase; MultiGameManager.INSTANCE.addGamePhaseToDimension(activePhase.dimension(), activePhase); activePhase.state().register(GameRewardsMap.STATE, ((GameLobby) lobby()).getRewardsMap()); - activePhase.state().register(VictoryPointsState.KEY, ((GameLobby) lobby()).createOrGetPoints(this)); + activePhase.state().register(RiverRaceState.KEY, (RiverRaceState) ((GameLobby) lobby()).createOrGetMultiPhaseState(this)); activePhase.start(saveInventory); } From a449cdb8a8962d9cb083381ed5af012bd5b89795 Mon Sep 17 00:00:00 2001 From: Cory Scheviak Date: Thu, 10 Oct 2024 20:32:12 +0200 Subject: [PATCH 05/15] Remove unused game phase type to fix builds --- .../minigames/client/lobby/state/ClientLobbyState.java | 2 +- .../lovetropics/minigames/common/core/game/GamePhaseType.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/lovetropics/minigames/client/lobby/state/ClientLobbyState.java b/src/main/java/com/lovetropics/minigames/client/lobby/state/ClientLobbyState.java index 354ca846..d7acbf81 100644 --- a/src/main/java/com/lovetropics/minigames/client/lobby/state/ClientLobbyState.java +++ b/src/main/java/com/lovetropics/minigames/client/lobby/state/ClientLobbyState.java @@ -58,7 +58,7 @@ public LobbyStatus getStatus() { return LobbyStatus.PAUSED; } return switch (currentGame.phase()) { - case PLAYING, PAUSED -> LobbyStatus.PLAYING; + case PLAYING -> LobbyStatus.PLAYING; case WAITING -> LobbyStatus.WAITING; }; } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java b/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java index 0b8f1f0d..90048898 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/GamePhaseType.java @@ -9,7 +9,6 @@ public enum GamePhaseType { PLAYING, // Players are loaded into and playing in a game world - PAUSED, // Game world is paused, but potentially child game worlds are now in PLAYING or WAITING WAITING, // Players are loaded into the 'waiting world' before being loaded into a game world ; From 3d9ba4d4f6a2ec9d41c1d9bb935573a0d30c3011 Mon Sep 17 00:00:00 2001 From: Cory Scheviak Date: Fri, 11 Oct 2024 21:46:17 +0200 Subject: [PATCH 06/15] Add river race translations file --- .../assets/ltminigames/lang/en_ud.json | 4 ++++ .../assets/ltminigames/lang/en_us.json | 4 ++++ .../lovetropics/minigames/LoveTropics.java | 2 ++ .../content/river_race/RiverRaceTexts.java | 20 +++++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java diff --git a/src/generated/resources/assets/ltminigames/lang/en_ud.json b/src/generated/resources/assets/ltminigames/lang/en_ud.json index ca57df38..e85f1b48 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_ud.json +++ b/src/generated/resources/assets/ltminigames/lang/en_ud.json @@ -15,6 +15,9 @@ "block.ltminigames.plastic_bottle": "ǝןʇʇoᗺ ɔıʇsɐןԀ", "block.ltminigames.plastic_rings": "sbuıᴚ ɔıʇsɐןԀ", "block.ltminigames.straw": "ʍɐɹʇS", + "block.ltminigames.trivia_collectable": "ǝןqɐʇɔǝןןoƆ ɐıʌıɹ⟘", + "block.ltminigames.trivia_gate": "ǝʇɐ⅁ ɐıʌıɹ⟘", + "block.ltminigames.trivia_reward": "pɹɐʍǝᴚ ɐıʌıɹ⟘", "effect.ltminigames.coin_multiplier_power_up": "dn-ɹǝʍoԀ ɹǝıןdıʇןnW uıoƆ", "effect.ltminigames.knockback_resistance_power_up": "d∩-ɹǝʍoԀ ǝɔuɐʇsısǝᴚ ʞɔɐqʞɔouʞ", "effect.ltminigames.leaky_pockets": "sʇǝʞɔoԀ ʎʞɐǝꞀ", @@ -382,6 +385,7 @@ "ltminigames.minigame.results": ":sʇןnsǝɹ ǝɥʇ ǝɹɐ ǝɹǝH ¡ɹǝʌo sı ǝɯɐb ǝɥ⟘", "ltminigames.minigame.reward_item": "%s x%s - ", "ltminigames.minigame.rewards_granted": "¡sǝɯɐbıuıɯ buıʎɐןd ɹoɟ spɹɐʍǝɹ ʇob noʎ", + "ltminigames.minigame.river_race": "ǝɔɐᴚ ɹǝʌıᴚ", "ltminigames.minigame.signature_run": "unᴚ ǝɹnʇɐubıS", "ltminigames.minigame.spectating_notification": "˙ɯooz oʇ ןןoɹɔs puɐ %s pןoH\n˙sɹǝʎɐןd ʇɔǝןǝs oʇ sʎǝʞ ʍoɹɹɐ ǝɥʇ ǝsn ɹo ןןoɹɔS\n¡%s ɐ ǝɹɐ noʎ", "ltminigames.minigame.spectating_notification.key": "ןoɹʇuoƆ ʇɟǝꞀ", diff --git a/src/generated/resources/assets/ltminigames/lang/en_us.json b/src/generated/resources/assets/ltminigames/lang/en_us.json index 1df521eb..450c8a94 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_us.json +++ b/src/generated/resources/assets/ltminigames/lang/en_us.json @@ -15,6 +15,9 @@ "block.ltminigames.plastic_bottle": "Plastic Bottle", "block.ltminigames.plastic_rings": "Plastic Rings", "block.ltminigames.straw": "Straw", + "block.ltminigames.trivia_collectable": "Trivia Collectable", + "block.ltminigames.trivia_gate": "Trivia Gate", + "block.ltminigames.trivia_reward": "Trivia Reward", "effect.ltminigames.coin_multiplier_power_up": "Coin Multiplier Power-up", "effect.ltminigames.knockback_resistance_power_up": "Knockback Resistance Power-Up", "effect.ltminigames.leaky_pockets": "Leaky Pockets", @@ -382,6 +385,7 @@ "ltminigames.minigame.results": "The game is over! Here are the results:", "ltminigames.minigame.reward_item": " - %sx %s", "ltminigames.minigame.rewards_granted": "You got rewards for playing minigames!", + "ltminigames.minigame.river_race": "River Race", "ltminigames.minigame.signature_run": "Signature Run", "ltminigames.minigame.spectating_notification": "You are a %s!\nScroll or use the arrow keys to select players.\nHold %s and scroll to zoom.", "ltminigames.minigame.spectating_notification.key": "Left Control", diff --git a/src/main/java/com/lovetropics/minigames/LoveTropics.java b/src/main/java/com/lovetropics/minigames/LoveTropics.java index d3f78be2..3e177393 100644 --- a/src/main/java/com/lovetropics/minigames/LoveTropics.java +++ b/src/main/java/com/lovetropics/minigames/LoveTropics.java @@ -19,6 +19,7 @@ import com.lovetropics.minigames.common.content.qottott.Qottott; import com.lovetropics.minigames.common.content.qottott.QottottTexts; import com.lovetropics.minigames.common.content.river_race.RiverRace; +import com.lovetropics.minigames.common.content.river_race.RiverRaceTexts; import com.lovetropics.minigames.common.content.spleef.Spleef; import com.lovetropics.minigames.common.content.survive_the_tide.SurviveTheTide; import com.lovetropics.minigames.common.content.survive_the_tide.SurviveTheTideTexts; @@ -120,6 +121,7 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { TrashDiveTexts.KEYS.forEach(consumer); TurtleRaceTexts.KEYS.forEach(consumer); QottottTexts.KEYS.forEach(consumer); + RiverRaceTexts.collectTranslations(consumer); }) .generic(TAB_ID.getPath(), Registries.CREATIVE_MODE_TAB, () -> CreativeModeTab.builder() .title(registrate().addLang("itemGroup", TAB_ID, "LTMinigames")) diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java new file mode 100644 index 00000000..cd491ee9 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java @@ -0,0 +1,20 @@ +package com.lovetropics.minigames.common.content.river_race; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.core.game.util.TranslationCollector; + +import java.util.function.BiConsumer; + +public final class RiverRaceTexts { + private static final TranslationCollector KEYS = new TranslationCollector(LoveTropics.ID + ".minigame.river_race."); + + static { + + } + + public static void collectTranslations(BiConsumer consumer) { + KEYS.forEach(consumer); + + consumer.accept(LoveTropics.ID + ".minigame.river_race", "River Race"); + } +} From efdeda4f0fa7097e6fb5d30f4fab3cc45bf4d043 Mon Sep 17 00:00:00 2001 From: Cory Scheviak Date: Fri, 11 Oct 2024 22:10:52 +0200 Subject: [PATCH 07/15] Add a trader to each zone --- .../common/content/river_race/RiverRace.java | 2 + .../content/river_race/RiverRaceTexts.java | 3 + .../behaviour/RiverRaceMerchantBehavior.java | 187 ++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceMerchantBehavior.java diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRace.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRace.java index 3484811a..0b1d4512 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRace.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRace.java @@ -2,6 +2,7 @@ import com.lovetropics.minigames.LoveTropics; import com.lovetropics.minigames.common.content.river_race.behaviour.MicrogamesBehaviour; +import com.lovetropics.minigames.common.content.river_race.behaviour.RiverRaceMerchantBehavior; import com.lovetropics.minigames.common.content.river_race.behaviour.TriviaBehaviour; import com.lovetropics.minigames.common.content.river_race.behaviour.VictoryPointsBehavior; import com.lovetropics.minigames.common.content.river_race.block.TriviaBlock; @@ -19,6 +20,7 @@ public class RiverRace { public static final GameBehaviorEntry TRIVIA_BEHAVIOUR = REGISTRATE.object("trivia").behavior(TriviaBehaviour.CODEC).register(); public static final GameBehaviorEntry MICROGAMES_BEHAVIOUR = REGISTRATE.object("microgames").behavior(MicrogamesBehaviour.CODEC).register(); public static final GameBehaviorEntry VICTORY_POINTS_BEHAVIOR = REGISTRATE.object("victory_points").behavior(VictoryPointsBehavior.CODEC).register(); + public static final GameBehaviorEntry RIVER_RACE_MERCHANT_BEHAVIOR = REGISTRATE.object("river_race_merchant").behavior(RiverRaceMerchantBehavior.CODEC).register(); public static final BlockEntry TRIVIA_GATE = REGISTRATE .block("trivia_gate", TriviaBlock.GateTriviaBlock::new) diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java index cd491ee9..a93e62e5 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceTexts.java @@ -2,12 +2,15 @@ import com.lovetropics.minigames.LoveTropics; import com.lovetropics.minigames.common.core.game.util.TranslationCollector; +import net.minecraft.network.chat.Component; import java.util.function.BiConsumer; public final class RiverRaceTexts { private static final TranslationCollector KEYS = new TranslationCollector(LoveTropics.ID + ".minigame.river_race."); + public static final Component SHOP = KEYS.add("shop", "Shop"); + static { } diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceMerchantBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceMerchantBehavior.java new file mode 100644 index 00000000..102452f8 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceMerchantBehavior.java @@ -0,0 +1,187 @@ +package com.lovetropics.minigames.common.content.river_race.behaviour; + +import com.lovetropics.lib.BlockBox; +import com.lovetropics.lib.codec.MoreCodecs; +import com.lovetropics.minigames.common.content.biodiversity_blitz.BiodiversityBlitzTexts; +import com.lovetropics.minigames.common.content.biodiversity_blitz.merchant.BbMerchant; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.ItemCost; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +// TODO Merge with BbMerchantBehavior? +public class RiverRaceMerchantBehavior implements IGameBehavior { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.STRING.fieldOf("zone").forGetter(c -> c.zone), + BuiltInRegistries.ENTITY_TYPE.byNameCodec().fieldOf("entity").forGetter(c -> c.entity), + ComponentSerialization.CODEC.optionalFieldOf("name", CommonComponents.EMPTY).forGetter(c -> c.name), + Offer.CODEC.listOf().fieldOf("offers").forGetter(c -> c.offers) + ).apply(i, RiverRaceMerchantBehavior::new)); + + private final String zone; + private final EntityType entity; + private final Component name; + private final List offers; + + private final Set merchants = new ObjectOpenHashSet<>(); + + private IGamePhase game; + + public RiverRaceMerchantBehavior(String zone, EntityType entity, Component name, List offers) { + this.zone = zone; + this.entity = entity; + this.name = name; + this.offers = offers; + } + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + this.game = game; + + events.listen(GamePhaseEvents.CREATE, this::onGameStarted); + events.listen(GamePlayerEvents.INTERACT_ENTITY, this::interactWithEntity); + } + + /** + * When the game loads, load this merchant into its proper section + */ + private void onGameStarted() { + ServerLevel world = game.level(); + + BlockBox region = game.mapRegions().getOrThrow(zone); + // if (region == null) return; + + Vec3 center = region.center(); + + Entity merchant = createMerchant(world); + if (merchant == null) return; + +// Direction direction = Util.getDirectionBetween(region, plot.spawn); +// float yaw = direction.toYRot(); +// + merchant.moveTo(center.x(), center.y() - 0.5, center.z(), 0 /*yaw*/, 0); +// merchant.setYHeadRot(yaw); + + world.getChunk(region.centerBlock()); + world.addFreshEntity(merchant); + + if (merchant instanceof Mob mob) { + mob.finalizeSpawn(world, world.getCurrentDifficultyAt(BlockPos.containing(center)), MobSpawnType.MOB_SUMMONED, null); + mob.setNoAi(true); + mob.setBaby(false); + mob.setInvulnerable(true); + } + + merchants.add(merchant.getUUID()); + } + + private InteractionResult interactWithEntity(ServerPlayer player, Entity target, InteractionHand hand) { + if (merchants.contains(target.getUUID())) { + MerchantOffers builtOffers = new MerchantOffers(); + for (Offer offer : offers) { + builtOffers.add(offer.build(game)); + } + + // TODO need a different screen? + BbMerchant merchant = new BbMerchant(player, builtOffers); + merchant.openTradingScreen(player, BiodiversityBlitzTexts.TRADING, 1); + + return InteractionResult.SUCCESS; + } + + return InteractionResult.PASS; + } + + @Nullable + private Entity createMerchant(ServerLevel world) { + Entity merchant = entity.create(world); + if (merchant != null) { + if (name != CommonComponents.EMPTY) { + merchant.setCustomName(name); + merchant.setCustomNameVisible(true); + } + + merchant.setSilent(true); + + return merchant; + } + + return null; + } + + public static final class Offer { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ItemCost.CODEC.fieldOf("input").forGetter(c -> c.input), + Output.CODEC.fieldOf("output").forGetter(c -> c.output) + ).apply(instance, Offer::new)); + + private final ItemCost input; + private final Output output; + + public Offer(ItemCost input, Output output) { + this.input = input; + this.output = output; + } + + public MerchantOffer build(IGamePhase game) { + return new MerchantOffer( + input, output.build(game), + Integer.MAX_VALUE, + 0, + 0 + ); + } + } + + public static final class Output { + private static final Codec CODEC = MoreCodecs.ITEM_STACK.xmap(Output::item, output -> output.item); + + @Nullable + private final ItemStack item; + + private Output(@Nullable ItemStack item) { + this.item = item; + } + + private static Output item(ItemStack item) { + return new Output(item); + } + + private ItemStack build(IGamePhase game) { + if (item != null) { + return item; + } + throw new IllegalStateException(); + } + } +} From 47881f84c57fa11fde9bd9f8b1e3fc8c33fadbf9 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sat, 12 Oct 2024 17:15:38 +0300 Subject: [PATCH 08/15] Add a client state to disable the recipe book --- .../river_race/behaviour/TriviaBehaviour.java | 3 +- .../client_state/GameClientStateTypes.java | 2 ++ .../instance/HideRecipeBookState.java | 18 ++++++++++ .../mixin/client/HiddenRecipeBookMixin.java | 34 ++++++++++++++++++ .../sprites/recipe_book/button_disabled.png | Bin 0 -> 396 bytes src/main/resources/ltminigames.mixins.json | 3 +- 6 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookState.java create mode 100644 src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java create mode 100644 src/main/resources/assets/ltminigames/textures/gui/sprites/recipe_book/button_disabled.png diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/TriviaBehaviour.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/TriviaBehaviour.java index 7c3be060..ed077bf7 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/TriviaBehaviour.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/TriviaBehaviour.java @@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public final class TriviaBehaviour implements IGameBehavior { @@ -42,7 +43,7 @@ public final class TriviaBehaviour implements IGameBehavior { ).apply(i, TriviaBehaviour::new)); private final List zones; private final int questionLockout; - private HashMap lockedOutTriviaBlocks = new HashMap<>(); + private final Map lockedOutTriviaBlocks = new ConcurrentHashMap<>(); public TriviaBehaviour(List zones, int questionLockout) { this.zones = zones; diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java index bb31cae5..98cccc72 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java @@ -5,6 +5,7 @@ import com.lovetropics.minigames.common.core.game.client_state.instance.FogClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.GlowTeamMembersState; import com.lovetropics.minigames.common.core.game.client_state.instance.HealthTagClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.HideRecipeBookState; import com.lovetropics.minigames.common.core.game.client_state.instance.PointTagClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.ReplaceTexturesClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.ResourcePackClientState; @@ -43,6 +44,7 @@ public final class GameClientStateTypes { public static final GameClientTweakEntry TEAM_MEMBERS = register("team_members", TeamMembersClientState.CODEC); public static final GameClientTweakEntry GLOW_TEAM_MEMBERS = register("glow_team_members", MapCodec.unit(GlowTeamMembersState.INSTANCE)); public static final GameClientTweakEntry POINT_TAGS = register("point_tags", PointTagClientState.CODEC); + public static final GameClientTweakEntry HIDE_RECIPE_BOOK = register("hide_recipe_book", HideRecipeBookState.CODEC); public static GameClientTweakEntry register(final String name, final MapCodec codec) { return REGISTRATE.object(name) diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookState.java new file mode 100644 index 00000000..4ab26155 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookState.java @@ -0,0 +1,18 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; + +public record HideRecipeBookState(Component message) implements GameClientState { + public static final MapCodec CODEC = ComponentSerialization.CODEC + .fieldOf("message").xmap(HideRecipeBookState::new, HideRecipeBookState::message); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.HIDE_RECIPE_BOOK.get(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java new file mode 100644 index 00000000..227f7b8e --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java @@ -0,0 +1,34 @@ +package com.lovetropics.minigames.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ImageButton; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.screens.inventory.CraftingScreen; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin({CraftingScreen.class, InventoryScreen.class}) +public class HiddenRecipeBookMixin { + @Unique + @WrapOperation(method = "init", at = @At(value = "NEW", target = "net/minecraft/client/gui/components/ImageButton")) + private ImageButton respectHiddenBook(int x, int y, int width, int height, WidgetSprites sprites, Button.OnPress onPress, Operation original) { + var disabled = ResourceLocation.fromNamespaceAndPath("ltminigames", "recipe_book/button_disabled"); + var org = original.call(x, y, width, height, new WidgetSprites( + sprites.enabled(), disabled, sprites.enabledFocused(), disabled + ), onPress); + var hidden = ClientGameStateManager.getOrNull(GameClientStateTypes.HIDE_RECIPE_BOOK); + if (hidden != null) { + org.active = false; + org.setTooltip(Tooltip.create(hidden.message())); + } + return org; + } +} diff --git a/src/main/resources/assets/ltminigames/textures/gui/sprites/recipe_book/button_disabled.png b/src/main/resources/assets/ltminigames/textures/gui/sprites/recipe_book/button_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..3d22601babc7c3af49c0dc76993efab7640b7c2e GIT binary patch literal 396 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q876k~CayA#8@b22Z19F}xPUq=Rp zjs4tz5?O(K*8raoS0JsUqob>LYze9E+8dd666>BA1h#Z=I|^IsD!h? zBeIx*f$uN~Gak=hkpdK4>*?Ycq7j^W;h@kV0}hr8dYJ;3LZYwy`M;cN$z#Xj%wK6I zd3N_~{3A1cV}loCScu!a*~dQ}a$2;C;b;Hy)%8mpw&u=Fo$kWXJgG_}g zUVU!#{B`xGn)&|vP|FE7Cr{5?_1(R1v)szPE9V`b>2Yn|LeJxt#~ZJ7U+EDtwhm6) z=dtL@70=M5nqJejtgeQ|-y@r5KE3xQwz9gr!d>4{Q|XIZNBH;nPZILK<+%io)h!7I PI-J4N)z4*}Q$iB}kUgEp literal 0 HcmV?d00001 diff --git a/src/main/resources/ltminigames.mixins.json b/src/main/resources/ltminigames.mixins.json index b9a20aeb..4397675e 100644 --- a/src/main/resources/ltminigames.mixins.json +++ b/src/main/resources/ltminigames.mixins.json @@ -28,7 +28,8 @@ "client.IntegratedPlayerListMixin", "client.MinecraftMixin", "client.PlayerTabOverlayGuiMixin", - "client.PoseStackAccessor" + "client.PoseStackAccessor", + "client.HiddenRecipeBookMixin" ], "injectors": { "defaultRequire": 1 From 2808bc045c01a05d7f2dd7a4cebe9959deb40e60 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sun, 13 Oct 2024 22:12:49 +0300 Subject: [PATCH 09/15] Crafting bee is in a working state --- build.gradle | 2 +- gradle.properties | 2 +- .../assets/ltminigames/lang/en_ud.json | 4 + .../assets/ltminigames/lang/en_us.json | 4 + .../games/action_trigger_test/events.json | 1 + .../games/action_trigger_test/start.json | 1 + .../games/action_trigger_test/stop.json | 1 + .../games/tweak_tests/cancel_damage.json | 1 + .../games/tweak_tests/disable_hunger.json | 1 + .../lttest/games/tweak_tests/max_health.json | 1 + .../games/tweak_tests/scale_damage.json | 1 + .../lovetropics/minigames/LoveTropics.java | 4 + .../game/handler/GameCraftingBeeHandler.java | 189 ++++++++++++++++++ .../common/content/MinigameTexts.java | 1 + .../content/crafting_bee/CraftingBee.java | 16 ++ .../crafting_bee/CraftingBeeBehavior.java | 183 +++++++++++++++++ .../crafting_bee/CraftingBeeTexts.java | 16 ++ .../content/crafting_bee/RecipeSelector.java | 58 ++++++ .../ingredient/FromRecipeDecomposer.java | 53 +++++ .../ingredient/IngredientDecomposer.java | 25 +++ .../PreferItemFromTagDecomposer.java | 40 ++++ .../ingredient/SimpleTagToItemDecomposer.java | 32 +++ .../common/core/game/IGamePhase.java | 5 + .../game/behavior/event/GamePlayerEvents.java | 13 ++ .../client_state/GameClientStateTypes.java | 2 + .../instance/CraftingBeeCrafts.java | 30 +++ .../core/game/impl/GameEventDispatcher.java | 10 + .../common/core/game/impl/GamePhase.java | 31 +++ .../mixin/client/HiddenRecipeBookMixin.java | 20 +- .../resources/META-INF/accesstransformer.cfg | 5 + .../gui/minigames/crafting_bee/items_bar.png | Bin 0 -> 218 bytes 31 files changed, 748 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBee.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/IngredientDecomposer.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/PreferItemFromTagDecomposer.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java create mode 100644 src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java create mode 100644 src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/items_bar.png diff --git a/build.gradle b/build.gradle index d3204aa5..bcfd053d 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'eclipse' id 'idea' id 'java-library' - id 'net.neoforged.moddev' version '1.0.0' + id 'net.neoforged.moddev' version '2.0.36-beta' } group = 'com.lovetropics.minigames' diff --git a/gradle.properties b/gradle.properties index a4dfe180..a9530e50 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ neo_version=21.0.167 parchment_version=2024.06.23 registrate_version=1.3.0+50 ltlib_version=[1.4.5,1.5) -ltextras_version=1.3.0-release+7 +ltextras_version=1.3.0-release+11 org.gradle.jvmargs=-Xmx1G org.gradle.daemon=true diff --git a/src/generated/resources/assets/ltminigames/lang/en_ud.json b/src/generated/resources/assets/ltminigames/lang/en_ud.json index e85f1b48..cf31e5af 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_ud.json +++ b/src/generated/resources/assets/ltminigames/lang/en_ud.json @@ -168,6 +168,8 @@ "ltminigames.minigame.calamity": "ʎʇıɯɐןɐƆ", "ltminigames.minigame.chaos_block_party": "ʎʇɹɐԀ ʞɔoןᗺ soɐɥƆ", "ltminigames.minigame.conservation_exploration": "uoıʇɐɹoןdxƎ uoıʇɐʌɹǝsuoƆ", + "ltminigames.minigame.crafting_bee.dont_cheat": "¡ʇɐǝɥɔ ʇ,uoᗡ", + "ltminigames.minigame.crafting_bee.team_has_completed_recipes": "sǝdıɔǝɹ %s ɟo ʇno %s pǝʇǝןdɯoɔ sɐɥ %s ɯɐǝ⟘", "ltminigames.minigame.donation.acid_rain": "uıɐᴚ pıɔⱯ", "ltminigames.minigame.donation.acid_rain.description": "¡ǝʇnuıɯ Ɩ ɹoɟ spɐǝɥ ,sɹǝʎɐןd ǝɥʇ uo uʍop pıɔɐ uıɐᴚ", "ltminigames.minigame.donation.acid_rain.toast": "¡ǝʇnuıɯ Ɩ ɹoɟ pıɔɐ buıuıɐɹ sı ʇI", @@ -386,6 +388,7 @@ "ltminigames.minigame.reward_item": "%s x%s - ", "ltminigames.minigame.rewards_granted": "¡sǝɯɐbıuıɯ buıʎɐןd ɹoɟ spɹɐʍǝɹ ʇob noʎ", "ltminigames.minigame.river_race": "ǝɔɐᴚ ɹǝʌıᴚ", + "ltminigames.minigame.river_race.shop": "doɥS", "ltminigames.minigame.signature_run": "unᴚ ǝɹnʇɐubıS", "ltminigames.minigame.spectating_notification": "˙ɯooz oʇ ןןoɹɔs puɐ %s pןoH\n˙sɹǝʎɐןd ʇɔǝןǝs oʇ sʎǝʞ ʍoɹɹɐ ǝɥʇ ǝsn ɹo ןןoɹɔS\n¡%s ɐ ǝɹɐ noʎ", "ltminigames.minigame.spectating_notification.key": "ןoɹʇuoƆ ʇɟǝꞀ", @@ -469,6 +472,7 @@ "ltminigames.minigame.survive_the_tide_intro5": "¡ǝǝs s,ʇǝꞀ\n", "ltminigames.minigame.survive_the_tide_pvp_enabled.subtitle": "˙˙˙sɹǝʎɐןd ɹǝɥʇo ɟo ǝɹɐʍǝᗺ", "ltminigames.minigame.survive_the_tide_pvp_enabled.title": "¡ᗡƎꞀᗺⱯNƎ SI ԀΛԀ", + "ltminigames.minigame.team_won": "¡ǝɯɐb ǝɥʇ uoʍ %s ɯɐǝ⟘ ⭐", "ltminigames.minigame.teams.blue": "ǝnןᗺ", "ltminigames.minigame.teams.green": "uǝǝɹ⅁", "ltminigames.minigame.teams.hiders": "sɹǝpıH", diff --git a/src/generated/resources/assets/ltminigames/lang/en_us.json b/src/generated/resources/assets/ltminigames/lang/en_us.json index 450c8a94..f124240e 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_us.json +++ b/src/generated/resources/assets/ltminigames/lang/en_us.json @@ -168,6 +168,8 @@ "ltminigames.minigame.calamity": "Calamity", "ltminigames.minigame.chaos_block_party": "Chaos Block Party", "ltminigames.minigame.conservation_exploration": "Conservation Exploration", + "ltminigames.minigame.crafting_bee.dont_cheat": "Don't cheat!", + "ltminigames.minigame.crafting_bee.team_has_completed_recipes": "Team %s has completed %s out of %s recipes", "ltminigames.minigame.donation.acid_rain": "Acid Rain", "ltminigames.minigame.donation.acid_rain.description": "Rain acid down on the players' heads for 1 minute!", "ltminigames.minigame.donation.acid_rain.toast": "It is raining acid for 1 minute!", @@ -386,6 +388,7 @@ "ltminigames.minigame.reward_item": " - %sx %s", "ltminigames.minigame.rewards_granted": "You got rewards for playing minigames!", "ltminigames.minigame.river_race": "River Race", + "ltminigames.minigame.river_race.shop": "Shop", "ltminigames.minigame.signature_run": "Signature Run", "ltminigames.minigame.spectating_notification": "You are a %s!\nScroll or use the arrow keys to select players.\nHold %s and scroll to zoom.", "ltminigames.minigame.spectating_notification.key": "Left Control", @@ -469,6 +472,7 @@ "ltminigames.minigame.survive_the_tide_intro5": "\nLet's see!", "ltminigames.minigame.survive_the_tide_pvp_enabled.subtitle": "Beware of other players...", "ltminigames.minigame.survive_the_tide_pvp_enabled.title": "PVP IS ENABLED!", + "ltminigames.minigame.team_won": "⭐ Team %s won the game!", "ltminigames.minigame.teams.blue": "Blue", "ltminigames.minigame.teams.green": "Green", "ltminigames.minigame.teams.hiders": "Hiders", diff --git a/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json b/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json index 2fc5a854..6fe72c75 100644 --- a/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json +++ b/src/generated/resources/testing/data/lttest/games/action_trigger_test/events.json @@ -19,6 +19,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json b/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json index 7fff7dc2..fefbf3b6 100644 --- a/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json +++ b/src/generated/resources/testing/data/lttest/games/action_trigger_test/start.json @@ -20,6 +20,7 @@ "volume": 0.5 } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json b/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json index 29213ea5..e36396c7 100644 --- a/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json +++ b/src/generated/resources/testing/data/lttest/games/action_trigger_test/stop.json @@ -16,6 +16,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json index 79341d30..c347f9d9 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/cancel_damage.json @@ -5,6 +5,7 @@ "type": "ltminigames:cancel_player_damage" } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json index ffc51ba0..389204c6 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/disable_hunger.json @@ -5,6 +5,7 @@ "type": "ltminigames:disable_hunger" } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json index ad4bb4ec..27c5679f 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/max_health.json @@ -12,6 +12,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json b/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json index ced2dbb8..06390d60 100644 --- a/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json +++ b/src/generated/resources/testing/data/lttest/games/tweak_tests/scale_damage.json @@ -12,6 +12,7 @@ } } ], + "is_multi_game": false, "map": { "type": "ltminigames:inline", "dimension": "minecraft:overworld" diff --git a/src/main/java/com/lovetropics/minigames/LoveTropics.java b/src/main/java/com/lovetropics/minigames/LoveTropics.java index 3e177393..0b7e01f2 100644 --- a/src/main/java/com/lovetropics/minigames/LoveTropics.java +++ b/src/main/java/com/lovetropics/minigames/LoveTropics.java @@ -15,6 +15,8 @@ import com.lovetropics.minigames.common.content.block_party.BlockParty; import com.lovetropics.minigames.common.content.block_party.BlockPartyTexts; import com.lovetropics.minigames.common.content.build_competition.BuildCompetition; +import com.lovetropics.minigames.common.content.crafting_bee.CraftingBee; +import com.lovetropics.minigames.common.content.crafting_bee.CraftingBeeTexts; import com.lovetropics.minigames.common.content.hide_and_seek.HideAndSeek; import com.lovetropics.minigames.common.content.qottott.Qottott; import com.lovetropics.minigames.common.content.qottott.QottottTexts; @@ -117,6 +119,7 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { MinigameTexts.KEYS.forEach(consumer); BiodiversityBlitzTexts.collectTranslations(consumer); BlockPartyTexts.KEYS.forEach(consumer); + CraftingBeeTexts.KEYS.forEach(consumer); SurviveTheTideTexts.KEYS.forEach(consumer); TrashDiveTexts.KEYS.forEach(consumer); TurtleRaceTexts.KEYS.forEach(consumer); @@ -145,6 +148,7 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { SurviveTheTide.init(); TrashDive.init(); BlockParty.init(); + CraftingBee.init(); TurtleRace.init(); Qottott.init(); Spleef.init(); diff --git a/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java new file mode 100644 index 00000000..5efcd865 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java @@ -0,0 +1,189 @@ +package com.lovetropics.minigames.client.game.handler; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.content.crafting_bee.CraftingBeeTexts; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.ChatFormatting; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.CraftingScreen; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ScreenEvent; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; + +@EventBusSubscriber(modid = LoveTropics.ID, value = Dist.CLIENT) +public class GameCraftingBeeHandler { + // TODO - hints + private static int hintsRemaining = 3; + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("ltminigames", "textures/gui/minigames/crafting_bee/items_bar.png"); + + @SubscribeEvent + static void onGuiInit(ScreenEvent.Init.Post event) { + if (getState() == null || !(event.getScreen() instanceof CraftingScreen screen)) return; + + event.addListener(new AbstractWidget(screen.getGuiLeft() + 22, screen.getGuiTop() - 21, 132, 21, Component.empty()) { + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + guiGraphics.blit(TEXTURE, this.getX(), this.getY(), 0, 0, 132, 21, 132, 21); + var crafts = getState().crafts(); + for (int i = 0; i < crafts.size(); i++) { + var craft = crafts.get(i); + var x = this.getX() + 4 + i * 18; + renderItem(guiGraphics, craft.output(), x, this.getY() + 4, 0, 0, 1f, 1f, 1f, craft.done() ? .1f : 1f); + + if (mouseX >= x && mouseX <= x + 16 && mouseY >= getY() + 4 && mouseY <= getY() + 20) { + var tooltipLines = new ArrayList<>(Screen.getTooltipFromItem(Minecraft.getInstance(), craft.output())); + if (craft.done()) { + tooltipLines.set(0, tooltipLines.get(0).copy().withStyle(ChatFormatting.GREEN)); + } else { + tooltipLines.add(CraftingBeeTexts.HINT); + tooltipLines.add(CraftingBeeTexts.HINTS_LEFT.apply(Component.literal(String.valueOf(hintsRemaining)).withStyle(ChatFormatting.AQUA))); + } + guiGraphics.renderTooltip(Minecraft.getInstance().font, tooltipLines, craft.output().getTooltipImage(), mouseX, mouseY); + } + } + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + + } + }); + } + + @Nullable + private static CraftingBeeCrafts getState() { + return ClientGameStateManager.getOrNull(GameClientStateTypes.CRAFTING_BEE_CRAFTS); + } + + public static void reset() { + + } + + private static void renderItem( + GuiGraphics graphics, ItemStack stack, int x, int y, int seed, int guiOffset, + float redTint, float greenTint, float blueTint, float alphaTint + ) { + if (!stack.isEmpty()) { + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + + BakedModel bakedmodel = Minecraft.getInstance().getItemRenderer().getModel(stack, null, null, seed); + graphics.pose().pushPose(); + graphics.pose().translate((float) (x + 8), (float) (y + 8), (float) (150 + (bakedmodel.isGui3d() ? guiOffset : 0))); + + try { + graphics.pose().scale(16.0F, -16.0F, 16.0F); + RenderSystem.applyModelViewMatrix(); + + boolean flag = !bakedmodel.usesBlockLight(); + if (flag) { + Lighting.setupForFlatItems(); + } + + Minecraft.getInstance() + .getItemRenderer() + .render(stack, ItemDisplayContext.GUI, false, graphics.pose(), renderType -> { + if (renderType instanceof RenderType.CompositeRenderType composite) { + if (composite.state().textureState instanceof RenderStateShard.TextureStateShard texture && texture.texture.isPresent()) { + return new TintedVertexConsumer( + graphics.bufferSource().getBuffer(RenderType.entityTranslucent(texture.texture.get())), redTint, greenTint, blueTint, alphaTint + ); + } + } + return new TintedVertexConsumer( + graphics.bufferSource().getBuffer(renderType), redTint, greenTint, blueTint, alphaTint + ); + }, LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY, bakedmodel); + graphics.flush(); + RenderSystem.enableDepthTest(); + if (flag) { + Lighting.setupFor3DItems(); + } + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.forThrowable(throwable, "Rendering item"); + CrashReportCategory crashreportcategory = crashreport.addCategory("Item being rendered"); + crashreportcategory.setDetail("Item Type", () -> String.valueOf(stack.getItem())); + crashreportcategory.setDetail("Item Components", () -> String.valueOf(stack.getComponents())); + crashreportcategory.setDetail("Item Foil", () -> String.valueOf(stack.hasFoil())); + throw new ReportedException(crashreport); + } + + graphics.pose().popPose(); + RenderSystem.applyModelViewMatrix(); + } + } + + public static final class TintedVertexConsumer implements VertexConsumer { + private final VertexConsumer wrapped; + + @Override + public VertexConsumer addVertex(float x, float y, float z) { + return wrapped.addVertex(x, y, z); + } + + @Override + public VertexConsumer setColor(int red, int green, int blue, int alpha) { + return wrapped.setColor((int)(red * this.red), (int)(green * this.green), (int)(blue * this.blue), (int)(alpha * this.alpha)); + } + + @Override + public VertexConsumer setUv(float u, float v) { + return wrapped.setUv(u, v); + } + + @Override + public VertexConsumer setUv1(int u, int v) { + return wrapped.setUv1(u, v); + } + + @Override + public VertexConsumer setUv2(int u, int v) { + return wrapped.setUv2(u, v); + } + + @Override + public VertexConsumer setNormal(float normalX, float normalY, float normalZ) { + return wrapped.setNormal(normalX, normalY, normalZ); + } + + private final float red; + private final float green; + private final float blue; + private final float alpha; + + public TintedVertexConsumer(VertexConsumer wrapped, float red, float green, float blue, float alpha) { + this.wrapped = wrapped; + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java b/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java index f93bea28..35f71235 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java +++ b/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java @@ -67,6 +67,7 @@ public final class MinigameTexts { public static final Component WINNER_TITLE = KEYS.add("winner.title", "WINNER"); public static final Component WINNER_SUBTITLE = KEYS.add("winner.subtitle", "You've emerged victorious!"); + public static final TranslationCollector.Fun1 TEAM_WON = KEYS.add1("team_won", "⭐ Team %s won the game!"); 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:"); diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBee.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBee.java new file mode 100644 index 00000000..84dc6082 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBee.java @@ -0,0 +1,16 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.util.registry.GameBehaviorEntry; +import com.lovetropics.minigames.common.util.registry.LoveTropicsRegistrate; + +public class CraftingBee { + private static final LoveTropicsRegistrate REGISTRATE = LoveTropics.registrate(); + + public static final GameBehaviorEntry CRAFTING_BEE = REGISTRATE.object("crafting_bee") + .behavior(CraftingBeeBehavior.CODEC) + .register(); + + public static void init() { + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java new file mode 100644 index 00000000..a2fff9e7 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java @@ -0,0 +1,183 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import com.lovetropics.minigames.common.content.MinigameTexts; +import com.lovetropics.minigames.common.content.crafting_bee.ingredient.IngredientDecomposer; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.GameStopReason; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.GameBehaviorType; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; +import com.lovetropics.minigames.common.core.game.player.PlayerSet; +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; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.ChatFormatting; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class CraftingBeeBehavior implements IGameBehavior { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + RecipeSelector.CODEC.codec().listOf().fieldOf("selectors").forGetter(c -> c.selectors), + IngredientDecomposer.CODEC.codec().listOf().fieldOf("decomposers").forGetter(c -> c.decomposers) + ).apply(in, CraftingBeeBehavior::new)); + + private final List selectors; + private final List decomposers; + + private TeamState teams; + private IGamePhase game; + + private ListMultimap tasks; + + public CraftingBeeBehavior(List selectors, List decomposers) { + this.selectors = selectors; + this.decomposers = decomposers; + } + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + tasks = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); + + decomposers.forEach(dec -> dec.prepareCache(game.level())); + + this.game = game; + teams = game.instanceState().getOrThrow(TeamState.KEY); + + events.listen(GamePhaseEvents.START, this::start); + + events.listen(GamePlayerEvents.CRAFT, this::onCraft); + } + + private void start() { + for (GameTeam team : teams) { + var recipes = selectors.stream().map(selector -> selector.select(game.level())) + .map(recipe -> new CraftingTask( + recipe.getResult(game.registryAccess()), + recipe + )) + .toList(); + tasks.putAll(team.key(), recipes); + sync(team.key()); + distributeIngredients(recipes, teams.getPlayersForTeam(team.key())); + } + } + + private void distributeIngredients(Collection tasks, PlayerSet players) { + // Empty teams have no players to distribute items to + if (players.isEmpty()) return; + + for (CraftingTask task : tasks) { + var ingredients = task.recipe.decompose(); + var items = ingredients.stream().flatMap(this::singleDecomposition).toList(); + + // Evenly distribute the items between the players + int p = 0; + var playerList = players.stream().toList(); + for (ItemStack item : items) { + playerList.get(p++).addItem(item.copy()); + if (p >= playerList.size()) p = 0; + } + } + } + + private Stream singleDecomposition(Ingredient ingredient) { + for (IngredientDecomposer decomposer : decomposers) { + var decomposed = decomposer.decompose(ingredient); + if (decomposed != null) { + return decomposed.stream().flatMap(this::singleDecomposition); + } + } + if (ingredient.getItems().length == 0) return Stream.empty(); + + // We have reduced the ingredient to its most basic form, so now we just pick the first item of the ingredient + for (ItemStack item : ingredient.getItems()) { + // Prioritize vanilla items + if (item.getItem().builtInRegistryHolder().key().location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { + return Stream.of(item); + } + } + return Stream.of(ingredient.getItems()[0]); + } + + private void onCraft(Player player, ItemStack crafted, Container container) { + var team = teams.getTeamForPlayer(player); + if (team == null) return; + + var teamTasks = tasks.get(team); + var task = teamTasks.stream().filter(c -> ItemStack.isSameItemSameComponents(crafted, c.output)).findFirst().orElse(null); + if (task == null) return; + + task.done = true; + + sync(team); + + var completed = teamTasks.stream().filter(t -> t.done).count(); + var teamName = teams.getTeamByKey(team).config().styledName(); + + game.allPlayers().sendMessage(CraftingBeeTexts.TEAM_HAS_COMPLETED_RECIPES.apply(teamName, completed, teamTasks.size())); + + if (completed == teamTasks.size()) { + + game.statistics().global().set(StatisticKey.WINNING_TEAM, team); + game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(teamName); + + game.allPlayers().forEach(ServerPlayer::closeContainer); + + game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamName).withStyle(ChatFormatting.GREEN), true)); + game.schedule(5, () -> game.requestStop(GameStopReason.finished())); + } + } + + private void sync(GameTeamKey team) { + teams.getPlayersForTeam(team).forEach(this::sync); + } + + private void sync(Player player) { + if (player instanceof ServerPlayer sp) { + GameClientState.sendToPlayer(new CraftingBeeCrafts(tasks.get(teams.getTeamForPlayer(player)).stream().map(CraftingTask::toCraft).toList()), sp); + } + } + + @Override + public Supplier> behaviorType() { + return CraftingBee.CRAFTING_BEE; + } + + private static class CraftingTask { + private final ItemStack output; + private final RecipeSelector.SelectedRecipe recipe; + private boolean done; + + private CraftingTask(ItemStack output, RecipeSelector.SelectedRecipe recipe) { + this.output = output; + this.recipe = recipe; + } + + public CraftingBeeCrafts.Craft toCraft() { + return new CraftingBeeCrafts.Craft(output, recipe.id(), done); + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java new file mode 100644 index 00000000..a571afcc --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java @@ -0,0 +1,16 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.content.MinigameTexts; +import com.lovetropics.minigames.common.core.game.util.TranslationCollector; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public final class CraftingBeeTexts { + public static final TranslationCollector KEYS = new TranslationCollector(LoveTropics.ID + ".minigame.crafting_bee."); + + public static final TranslationCollector.Fun3 TEAM_HAS_COMPLETED_RECIPES = KEYS.add3("team_has_completed_recipes", "Team %s has completed %s out of %s recipes"); + public static final Component DONT_CHEAT = KEYS.add("dont_cheat", "Don't cheat!").withStyle(ChatFormatting.RED); + public static final Component HINT = MinigameTexts.KEYS.add("hint", "Click to show a hint, displaying the position of a random number of ingredients"); + public static final TranslationCollector.Fun1 HINTS_LEFT = MinigameTexts.KEYS.add1("hints_left", "You have %s hints left"); +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java new file mode 100644 index 00000000..a72dfa02 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java @@ -0,0 +1,58 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.Util; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.ShapelessRecipe; + +import java.util.List; + +public interface RecipeSelector { + BiMap> TYPES = ImmutableBiMap.of("from_list", FromList.CODEC); + MapCodec CODEC = Codec.STRING.dispatchMap(s -> TYPES.inverse().get(s.getType()), TYPES::get); + + SelectedRecipe select(ServerLevel level); + + MapCodec getType(); + + record SelectedRecipe(ResourceLocation id, Either recipe) { + public SelectedRecipe(RecipeHolder holder) { + this(holder.id(), holder.value() instanceof ShapedRecipe sr ? Either.left(sr) : Either.right((ShapelessRecipe) holder.value())); + } + + public ItemStack getResult(RegistryAccess access) { + return recipe.map(rp -> rp.getResultItem(access), rp -> rp.getResultItem(access)); + } + + public List decompose() { + return recipe.map(ShapedRecipe::getIngredients, ShapelessRecipe::getIngredients); + } + } + + record FromList(List recipes) implements RecipeSelector { + public static final MapCodec CODEC = ResourceLocation.CODEC.listOf().fieldOf("recipes") + .xmap(FromList::new, FromList::recipes); + + @Override + public SelectedRecipe select(ServerLevel level) { + var key = Util.getRandom(recipes, level.getRandom()); + var recipe = level.getRecipeManager().byKey(key).orElseThrow(() -> new NullPointerException("Recipe " + key + " doesn't exist")); + return new SelectedRecipe(recipe); + } + + @Override + public MapCodec getType() { + return CODEC; + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java new file mode 100644 index 00000000..3f7f7989 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java @@ -0,0 +1,53 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.lovetropics.minigames.common.content.crafting_bee.RecipeSelector; +import com.mojang.serialization.MapCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +public class FromRecipeDecomposer implements IngredientDecomposer { + public static final MapCodec CODEC = ResourceLocation.CODEC.listOf() + .fieldOf("recipes").xmap(FromRecipeDecomposer::new, d -> d.recipes); + + private final List recipes; + + private final Map> cache = new IdentityHashMap<>(); + + public FromRecipeDecomposer(List recipes) { + this.recipes = recipes; + } + + @Override + public @Nullable List decompose(Ingredient ingredient) { + if (ingredient.getValues().length == 1 && ingredient.getValues()[0] instanceof Ingredient.ItemValue(ItemStack item)) { + return cache.get(item.getItem()); + } + return null; + } + + @Override + public void prepareCache(ServerLevel level) { + cache.clear(); + + for (ResourceLocation recipe : recipes) { + level.getServer().getRecipeManager().byKey(recipe).map(RecipeSelector.SelectedRecipe::new) + .ifPresent(r -> cache.put( + r.getResult(level.registryAccess()).getItem(), + r.decompose() + )); + } + } + + @Override + public MapCodec codec() { + return CODEC; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/IngredientDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/IngredientDecomposer.java new file mode 100644 index 00000000..a34a341a --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/IngredientDecomposer.java @@ -0,0 +1,25 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface IngredientDecomposer { + BiMap> TYPES = ImmutableBiMap.of("prefer_from_tag", PreferItemFromTagDecomposer.CODEC, + "from_recipe", FromRecipeDecomposer.CODEC, + "simple_tag", SimpleTagToItemDecomposer.CODEC); + MapCodec CODEC = Codec.STRING.dispatchMap(s -> TYPES.inverse().get(s.codec()), TYPES::get); + + @Nullable + List decompose(Ingredient ingredient); + + default void prepareCache(ServerLevel level) {} + + MapCodec codec(); +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/PreferItemFromTagDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/PreferItemFromTagDecomposer.java new file mode 100644 index 00000000..ad2e6160 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/PreferItemFromTagDecomposer.java @@ -0,0 +1,40 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +public record PreferItemFromTagDecomposer(Map, Item> preferences) implements IngredientDecomposer { + public PreferItemFromTagDecomposer(Map, Item> preferences) { + this.preferences = new IdentityHashMap<>(preferences); + } + + public static final MapCodec CODEC = Codec.unboundedMap( + TagKey.hashedCodec(Registries.ITEM), BuiltInRegistries.ITEM.byNameCodec() + ).fieldOf("preferences").xmap(PreferItemFromTagDecomposer::new, PreferItemFromTagDecomposer::preferences); + + @Override + public @Nullable List decompose(Ingredient ingredient) { + if (ingredient.getValues().length == 1 && ingredient.getValues()[0] instanceof Ingredient.TagValue(TagKey tag)) { + var pref = preferences.get(tag); + if (pref != null) { + return List.of(Ingredient.of(pref)); + } + } + return null; + } + + @Override + public MapCodec codec() { + return CODEC; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java new file mode 100644 index 00000000..ac48ed9d --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java @@ -0,0 +1,32 @@ +package com.lovetropics.minigames.common.content.crafting_bee.ingredient; + +import com.mojang.serialization.MapCodec; +import net.minecraft.world.item.crafting.Ingredient; +import net.neoforged.neoforge.common.crafting.DifferenceIngredient; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public record SimpleTagToItemDecomposer() implements IngredientDecomposer { + public static final MapCodec CODEC = MapCodec.unit(SimpleTagToItemDecomposer::new); + + @Override + public @Nullable List decompose(Ingredient ingredient) { + // This is a "hack". Neo will sometimes replace a vanilla recipe with a difference ingredient (#chests - #chests/trapped) + // we just resolve it and return the first item + if (ingredient.getCustomIngredient() instanceof DifferenceIngredient) { + return List.of(Ingredient.of(ingredient.getItems()[0])); + } else if (ingredient.getValues().length == 1 && ingredient.getValues()[0] instanceof Ingredient.TagValue) { + var items = ingredient.getValues()[0].getItems(); + if (items.size() == 1) { + return items.stream().map(Ingredient::of).toList(); + } + } + return null; + } + + @Override + public MapCodec codec() { + return CODEC; + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java index 8212d1c2..fe71d250 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/IGamePhase.java @@ -66,6 +66,11 @@ default GameStateMap instanceState() { GameResult requestStop(GameStopReason reason); + /** + * Schedule a task to be run after the specified amount of seconds. + */ + void schedule(float seconds, Runnable task); + /** * Adds the player to this game instance with the given role, or if already in the change, changes their role. * The given player will be removed from their former role, if any. diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java index 2a0cebb1..4a557829 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GamePlayerEvents.java @@ -7,11 +7,14 @@ import net.minecraft.network.chat.PlayerChatMessage; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; @@ -211,6 +214,12 @@ public final class GamePlayerEvents { } }); + public static final GameEventType CRAFT = GameEventType.create(Craft.class, listeners -> (player, item, container) -> { + for (Craft listener : listeners) { + listener.onCraft(player, item, container); + } + }); + private GamePlayerEvents() { } @@ -297,4 +306,8 @@ public interface Chat { public interface Return { void onReturn(UUID playerId, @Nullable PlayerRole role); } + + public interface Craft { + void onCraft(Player player, ItemStack crafted, Container craftingContainer); + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java index 98cccc72..61b2693c 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java @@ -2,6 +2,7 @@ import com.lovetropics.minigames.LoveTropics; import com.lovetropics.minigames.common.core.game.client_state.instance.BeaconClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; import com.lovetropics.minigames.common.core.game.client_state.instance.FogClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.GlowTeamMembersState; import com.lovetropics.minigames.common.core.game.client_state.instance.HealthTagClientState; @@ -45,6 +46,7 @@ public final class GameClientStateTypes { public static final GameClientTweakEntry GLOW_TEAM_MEMBERS = register("glow_team_members", MapCodec.unit(GlowTeamMembersState.INSTANCE)); public static final GameClientTweakEntry POINT_TAGS = register("point_tags", PointTagClientState.CODEC); public static final GameClientTweakEntry HIDE_RECIPE_BOOK = register("hide_recipe_book", HideRecipeBookState.CODEC); + public static final GameClientTweakEntry CRAFTING_BEE_CRAFTS = register("crafting_bee_crafts", CraftingBeeCrafts.CODEC); public static GameClientTweakEntry register(final String name, final MapCodec codec) { return REGISTRATE.object(name) diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java new file mode 100644 index 00000000..fd185747 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java @@ -0,0 +1,30 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +public record CraftingBeeCrafts(List crafts) implements GameClientState { + public static final MapCodec CODEC = Craft.CODEC.listOf() + .fieldOf("crafts").xmap(CraftingBeeCrafts::new, CraftingBeeCrafts::crafts); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.CRAFTING_BEE_CRAFTS.get(); + } + + public record Craft(ItemStack output, ResourceLocation recipeId, boolean done) { + public static final Codec CODEC = RecordCodecBuilder.create(in -> in.group( + ItemStack.CODEC.fieldOf("output").forGetter(Craft::output), + ResourceLocation.CODEC.fieldOf("recipe").forGetter(Craft::recipeId), + Codec.BOOL.fieldOf("done").forGetter(Craft::done) + ).apply(in, Craft::new)); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameEventDispatcher.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameEventDispatcher.java index 4aeb52a3..a38694e2 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameEventDispatcher.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameEventDispatcher.java @@ -180,6 +180,16 @@ public void onDeath(LivingDeathEvent event) { } } + @SubscribeEvent + public void onPlayerCraft(PlayerEvent.ItemCraftedEvent event) { + var entity = event.getEntity(); + + IGamePhase game = gameLookup.getGamePhaseFor(entity); + if (game != null) { + game.invoker(GamePlayerEvents.CRAFT).onCraft(entity, event.getCrafting(), event.getInventory()); + } + } + @SubscribeEvent public void onMobDrop(LivingDropsEvent event) { LivingEntity entity = event.getEntity(); diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java index 6e732d07..b41bedb6 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java @@ -37,6 +37,7 @@ import javax.annotation.Nullable; import java.util.Collections; import java.util.EnumMap; +import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.UUID; @@ -65,6 +66,8 @@ public class GamePhase implements IGamePhase { GameStopReason stopped; boolean destroyed; + private final LinkedList pendingRunnables = new LinkedList<>(); + protected GamePhase(GameInstance game, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors) { this.game = game; server = game.server(); @@ -154,6 +157,19 @@ protected ServerPlayer addAndSpawnPlayer(ServerPlayer player, @Nullable PlayerRo @Nullable GameStopReason tick() { try { + if (!pendingRunnables.isEmpty()) { + var itr = pendingRunnables.iterator(); + while (itr.hasNext()) { + var task = itr.next(); + if (task.counter <= 0) { + task.toRun.run(); + itr.remove(); + } else { + task.counter--; + } + } + } + invoker(GamePhaseEvents.TICK).tick(); } catch (Exception e) { cancelWithError(e); @@ -161,6 +177,11 @@ GameStopReason tick() { return stopped; } + @Override + public void schedule(float seconds, Runnable task) { + pendingRunnables.add(new ScheduledTask(task, (int)(server().tickRateManager().tickrate() * seconds))); + } + @Override public IGame game() { return game; @@ -323,4 +344,14 @@ public long ticks() { public boolean isActive() { return !destroyed; } + + private static class ScheduledTask { + private final Runnable toRun; + private int counter; + + private ScheduledTask(Runnable toRun, int counter) { + this.toRun = toRun; + this.counter = counter; + } + } } diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java index 227f7b8e..e56a694a 100644 --- a/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java +++ b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java @@ -4,20 +4,36 @@ import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.lovetropics.minigames.client.game.ClientGameStateManager; import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.ImageButton; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.WidgetSprites; import net.minecraft.client.gui.screens.inventory.CraftingScreen; import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.RecipeBookType; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin({CraftingScreen.class, InventoryScreen.class}) public class HiddenRecipeBookMixin { - @Unique + @Final + @Shadow(remap = false) + private RecipeBookComponent recipeBookComponent; + + @Inject(method = "init", at = @At("HEAD")) + private void hideBookIfOpen(CallbackInfo ci) { + if (ClientGameStateManager.getOrNull(GameClientStateTypes.HIDE_RECIPE_BOOK) != null && recipeBookComponent.isVisible()) { + Minecraft.getInstance().player.getRecipeBook().setBookSetting(RecipeBookType.CRAFTING, false, false); + } + } + @WrapOperation(method = "init", at = @At(value = "NEW", target = "net/minecraft/client/gui/components/ImageButton")) private ImageButton respectHiddenBook(int x, int y, int width, int height, WidgetSprites sprites, Button.OnPress onPress, Operation original) { var disabled = ResourceLocation.fromNamespaceAndPath("ltminigames", "recipe_book/button_disabled"); diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 32bdb918..a8c47f49 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -19,3 +19,8 @@ public net.minecraft.world.entity.monster.Creeper explodeCreeper()V # Collisions public net.minecraft.world.entity.Entity collideWithShapes(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/List;)Lnet/minecraft/world/phys/Vec3; + +public net.minecraft.client.renderer.RenderType$CompositeRenderType +public net.minecraft.client.renderer.RenderType$CompositeRenderType state()Lnet/minecraft/client/renderer/RenderType$CompositeState; +public net.minecraft.client.renderer.RenderType$CompositeState textureState +public net.minecraft.client.renderer.RenderStateShard$TextureStateShard texture diff --git a/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/items_bar.png b/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/items_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..d353aad4c51dca3c5ae53858358a850ae37b6d40 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^EkG>F!3-ofEZ9~Iq!^2X+?^QKos)S9^9T5ZxB}__|NkF5b}TeB6v*bfnDq-tv6ck+1p~$ZGyLAh7Yr2OEbxc~ z8pwATgc* Date: Sun, 13 Oct 2024 23:22:56 +0300 Subject: [PATCH 10/15] Add hints --- .../assets/ltminigames/lang/en_ud.json | 2 + .../assets/ltminigames/lang/en_us.json | 2 + .../game/handler/GameCraftingBeeHandler.java | 138 +++++++++++++++++- .../crafting_bee/CraftingBeeBehavior.java | 26 ++-- .../crafting_bee/CraftingBeeTexts.java | 5 +- .../content/crafting_bee/RecipeSelector.java | 21 --- .../content/crafting_bee/SelectedRecipe.java | 31 ++++ .../ingredient/FromRecipeDecomposer.java | 4 +- .../instance/CraftingBeeCrafts.java | 12 +- .../mixin/client/HiddenRecipeBookMixin.java | 2 +- .../minigames/crafting_bee/crafting_grid.png | Bin 0 -> 171 bytes 11 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/lovetropics/minigames/common/content/crafting_bee/SelectedRecipe.java create mode 100644 src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/crafting_grid.png diff --git a/src/generated/resources/assets/ltminigames/lang/en_ud.json b/src/generated/resources/assets/ltminigames/lang/en_ud.json index cf31e5af..a71c70a2 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_ud.json +++ b/src/generated/resources/assets/ltminigames/lang/en_ud.json @@ -169,6 +169,8 @@ "ltminigames.minigame.chaos_block_party": "ʎʇɹɐԀ ʞɔoןᗺ soɐɥƆ", "ltminigames.minigame.conservation_exploration": "uoıʇɐɹoןdxƎ uoıʇɐʌɹǝsuoƆ", "ltminigames.minigame.crafting_bee.dont_cheat": "¡ʇɐǝɥɔ ʇ,uoᗡ", + "ltminigames.minigame.crafting_bee.hint": "sʇuǝıpǝɹbuı ɟo ɹǝqɯnu ɯopuɐɹ ɐ ɟo uoıʇısod ǝɥʇ buıʎɐןdsıp 'ʇuıɥ ɐ ʍoɥs oʇ ʞɔıןƆ", + "ltminigames.minigame.crafting_bee.hints_left": "ʇɟǝן sʇuıɥ %s ǝʌɐɥ noʎ", "ltminigames.minigame.crafting_bee.team_has_completed_recipes": "sǝdıɔǝɹ %s ɟo ʇno %s pǝʇǝןdɯoɔ sɐɥ %s ɯɐǝ⟘", "ltminigames.minigame.donation.acid_rain": "uıɐᴚ pıɔⱯ", "ltminigames.minigame.donation.acid_rain.description": "¡ǝʇnuıɯ Ɩ ɹoɟ spɐǝɥ ,sɹǝʎɐןd ǝɥʇ uo uʍop pıɔɐ uıɐᴚ", diff --git a/src/generated/resources/assets/ltminigames/lang/en_us.json b/src/generated/resources/assets/ltminigames/lang/en_us.json index f124240e..1d748da8 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_us.json +++ b/src/generated/resources/assets/ltminigames/lang/en_us.json @@ -169,6 +169,8 @@ "ltminigames.minigame.chaos_block_party": "Chaos Block Party", "ltminigames.minigame.conservation_exploration": "Conservation Exploration", "ltminigames.minigame.crafting_bee.dont_cheat": "Don't cheat!", + "ltminigames.minigame.crafting_bee.hint": "Click to show a hint, displaying the position of a random number of ingredients", + "ltminigames.minigame.crafting_bee.hints_left": "You have %s hints left", "ltminigames.minigame.crafting_bee.team_has_completed_recipes": "Team %s has completed %s out of %s recipes", "ltminigames.minigame.donation.acid_rain": "Acid Rain", "ltminigames.minigame.donation.acid_rain.description": "Rain acid down on the players' heads for 1 minute!", diff --git a/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java index 5efcd865..0bc3d57c 100644 --- a/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java +++ b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java @@ -3,6 +3,7 @@ import com.lovetropics.minigames.LoveTropics; import com.lovetropics.minigames.client.game.ClientGameStateManager; import com.lovetropics.minigames.common.content.crafting_bee.CraftingBeeTexts; +import com.lovetropics.minigames.common.content.crafting_bee.SelectedRecipe; import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; import com.mojang.blaze3d.platform.GlStateManager; @@ -14,38 +15,98 @@ import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.CraftingScreen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.RenderStateShard; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.core.NonNullList; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RegisterClientTooltipComponentFactoriesEvent; import net.neoforged.neoforge.client.event.ScreenEvent; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.function.Predicate; @EventBusSubscriber(modid = LoveTropics.ID, value = Dist.CLIENT) public class GameCraftingBeeHandler { - // TODO - hints - private static int hintsRemaining = 3; + private static int hintsRemaining; + private static UUID lastKnownGame; + private static Map hintGrids; + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("ltminigames", "textures/gui/minigames/crafting_bee/items_bar.png"); + private static final ResourceLocation GRID_TEXTURE = ResourceLocation.fromNamespaceAndPath("ltminigames", "textures/gui/minigames/crafting_bee/crafting_grid.png"); + + @EventBusSubscriber(modid = LoveTropics.ID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD) + public static class ModSubscriber { + @SubscribeEvent + static void onRegisterTooltips(final RegisterClientTooltipComponentFactoriesEvent event) { + event.register(RecipeHint.class, recipeHint -> new ClientTooltipComponent() { + @Override + public int getHeight() { + return 58; + } + + @Override + public int getWidth(Font font) { + return 54; + } + + @Override + public void renderImage(Font font, int x, int y, GuiGraphics guiGraphics) { + guiGraphics.blit(GRID_TEXTURE, x, y, 0, 0, 54, 54, 54, 54); + for (int i = 0; i < recipeHint.grid().size(); i++) { + var ingredient = recipeHint.grid.get(i); + if (ingredient.isEmpty()) continue; + + var size = recipeHint.grid.size() == 4 ? 2 : 3; + + guiGraphics.renderFakeItem( + resolveIngredient(ingredient), + x + 1 + 18 * (i % size), + y + 1 + 18 * (i / size) + ); + } + } + }); + } + } @SubscribeEvent static void onGuiInit(ScreenEvent.Init.Post event) { if (getState() == null || !(event.getScreen() instanceof CraftingScreen screen)) return; + var state = getState(); + if (!Objects.equals(state.gameId(), lastKnownGame)) { + lastKnownGame = state.gameId(); + hintsRemaining = state.allowedHints(); + hintGrids = new HashMap<>(); + } + event.addListener(new AbstractWidget(screen.getGuiLeft() + 22, screen.getGuiTop() - 21, 132, 21, Component.empty()) { @Override protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { @@ -57,16 +118,66 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo renderItem(guiGraphics, craft.output(), x, this.getY() + 4, 0, 0, 1f, 1f, 1f, craft.done() ? .1f : 1f); if (mouseX >= x && mouseX <= x + 16 && mouseY >= getY() + 4 && mouseY <= getY() + 20) { + var hint = hintGrids.get(craft.recipeId()); + var tooltipLines = new ArrayList<>(Screen.getTooltipFromItem(Minecraft.getInstance(), craft.output())); if (craft.done()) { tooltipLines.set(0, tooltipLines.get(0).copy().withStyle(ChatFormatting.GREEN)); - } else { + } else if (hint == null || hint.expectedIngredientCount() != hint.grid().stream().filter(Predicate.not(Ingredient::isEmpty)).count()) { tooltipLines.add(CraftingBeeTexts.HINT); tooltipLines.add(CraftingBeeTexts.HINTS_LEFT.apply(Component.literal(String.valueOf(hintsRemaining)).withStyle(ChatFormatting.AQUA))); } - guiGraphics.renderTooltip(Minecraft.getInstance().font, tooltipLines, craft.output().getTooltipImage(), mouseX, mouseY); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tooltipLines, Optional.ofNullable(hint).filter($ -> !craft.done()), mouseX, mouseY); + } + } + } + + @Override + public void onClick(double mouseX, double mouseY, int button) { + if (hintsRemaining <= 0) return; + + var crafts = getState().crafts(); + + if (mouseY < getY() + 4 || mouseY > getY() + 4 + 16) return; + if (mouseX < getX() + 4 || mouseX > getX() + 4 + (18 * crafts.size() - 1)) return; + var index = (int)(mouseX - getX() - 4) / 18; + + var craft = crafts.get(index); + if (craft.done()) return; + + var recipe = new SelectedRecipe(craft.recipeId(), Minecraft.getInstance().player.connection.getRecipeManager()); + var ingredients = recipe.decompose(); + + var grid = hintGrids.computeIfAbsent(craft.recipeId(), k -> new RecipeHint( + NonNullList.withSize( + recipe.recipe().map(shaped -> shaped.getWidth() * shaped.getHeight(), shapeless -> shapeless.getIngredients().size() > 3 ? 9 : shapeless.getIngredients().size()), + Ingredient.EMPTY), + (int)ingredients.stream().filter(Predicate.not(Ingredient::isEmpty)).count() + )); + + int filledGridAmount = (int) grid.grid().stream().filter(Predicate.not(Ingredient::isEmpty)).count(); + if (grid.expectedIngredientCount() == filledGridAmount) return; + + record PositionedIngredient(Ingredient ingredient, int position) {} + + List ingredientsToPick = new ArrayList<>(); + for (int i = 0; i < ingredients.size(); i++) { + var ingr = ingredients.get(i); + if (!ingr.isEmpty() && grid.grid().get(i).isEmpty()) { + ingredientsToPick.add(new PositionedIngredient(ingr, i)); } } + + Collections.shuffle(ingredientsToPick); + // Make sure that we never show the full recipe in just one hint + var ingredientsToShow = new Random().nextInt(filledGridAmount == 0 ? Math.max(1, ingredientsToPick.size() - 1) : ingredientsToPick.size()); + + for (int i = 0; i <= ingredientsToShow; i++) { + var ingredient = ingredientsToPick.get(i); + grid.grid().set(ingredient.position(), ingredient.ingredient()); + } + + hintsRemaining--; } @Override @@ -81,6 +192,19 @@ private static CraftingBeeCrafts getState() { return ClientGameStateManager.getOrNull(GameClientStateTypes.CRAFTING_BEE_CRAFTS); } + private static ItemStack resolveIngredient(Ingredient ingredient) { + if (ingredient.isEmpty()) { + return ItemStack.EMPTY; + } + for (ItemStack item : ingredient.getItems()) { + // Prioritize vanilla items + if (item.getItem().builtInRegistryHolder().key().location().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { + return item; + } + } + return ingredient.getItems()[0]; + } + public static void reset() { } @@ -186,4 +310,10 @@ public TintedVertexConsumer(VertexConsumer wrapped, float red, float green, floa this.alpha = alpha; } } + + public record RecipeHint( + NonNullList grid, + int expectedIngredientCount + ) implements TooltipComponent {} + } diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java index a2fff9e7..1dce916c 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java @@ -20,11 +20,11 @@ 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; +import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.ChatFormatting; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.TickTask; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; import net.minecraft.world.entity.player.Player; @@ -33,28 +33,33 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; public class CraftingBeeBehavior implements IGameBehavior { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( RecipeSelector.CODEC.codec().listOf().fieldOf("selectors").forGetter(c -> c.selectors), - IngredientDecomposer.CODEC.codec().listOf().fieldOf("decomposers").forGetter(c -> c.decomposers) + IngredientDecomposer.CODEC.codec().listOf().fieldOf("decomposers").forGetter(c -> c.decomposers), + Codec.INT.optionalFieldOf("hints_per_player", 3).forGetter(c -> c.allowedHints) ).apply(in, CraftingBeeBehavior::new)); private final List selectors; private final List decomposers; + private final int allowedHints; private TeamState teams; private IGamePhase game; private ListMultimap tasks; - public CraftingBeeBehavior(List selectors, List decomposers) { + public CraftingBeeBehavior(List selectors, List decomposers, int allowedHints) { this.selectors = selectors; this.decomposers = decomposers; + this.allowedHints = allowedHints; } @Override @@ -91,7 +96,8 @@ private void distributeIngredients(Collection tasks, PlayerSet pla for (CraftingTask task : tasks) { var ingredients = task.recipe.decompose(); - var items = ingredients.stream().flatMap(this::singleDecomposition).toList(); + var items = ingredients.stream().flatMap(this::singleDecomposition).collect(Collectors.toCollection(ArrayList::new)); + Collections.shuffle(items); // Evenly distribute the items between the players int p = 0; @@ -104,13 +110,14 @@ private void distributeIngredients(Collection tasks, PlayerSet pla } private Stream singleDecomposition(Ingredient ingredient) { + if (ingredient.isEmpty()) return Stream.empty(); + for (IngredientDecomposer decomposer : decomposers) { var decomposed = decomposer.decompose(ingredient); if (decomposed != null) { return decomposed.stream().flatMap(this::singleDecomposition); } } - if (ingredient.getItems().length == 0) return Stream.empty(); // We have reduced the ingredient to its most basic form, so now we just pick the first item of the ingredient for (ItemStack item : ingredient.getItems()) { @@ -128,7 +135,7 @@ private void onCraft(Player player, ItemStack crafted, Container container) { var teamTasks = tasks.get(team); var task = teamTasks.stream().filter(c -> ItemStack.isSameItemSameComponents(crafted, c.output)).findFirst().orElse(null); - if (task == null) return; + if (task == null || task.done) return; task.done = true; @@ -157,7 +164,8 @@ private void sync(GameTeamKey team) { private void sync(Player player) { if (player instanceof ServerPlayer sp) { - GameClientState.sendToPlayer(new CraftingBeeCrafts(tasks.get(teams.getTeamForPlayer(player)).stream().map(CraftingTask::toCraft).toList()), sp); + GameClientState.sendToPlayer(new CraftingBeeCrafts(tasks.get(teams.getTeamForPlayer(player)).stream().map(CraftingTask::toCraft).toList(), + game.gameUuid(), allowedHints), sp); } } @@ -168,10 +176,10 @@ public Supplier> behaviorType() { private static class CraftingTask { private final ItemStack output; - private final RecipeSelector.SelectedRecipe recipe; + private final SelectedRecipe recipe; private boolean done; - private CraftingTask(ItemStack output, RecipeSelector.SelectedRecipe recipe) { + private CraftingTask(ItemStack output, SelectedRecipe recipe) { this.output = output; this.recipe = recipe; } diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java index a571afcc..9c4fd32c 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeTexts.java @@ -1,7 +1,6 @@ package com.lovetropics.minigames.common.content.crafting_bee; import com.lovetropics.minigames.LoveTropics; -import com.lovetropics.minigames.common.content.MinigameTexts; import com.lovetropics.minigames.common.core.game.util.TranslationCollector; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; @@ -11,6 +10,6 @@ public final class CraftingBeeTexts { public static final TranslationCollector.Fun3 TEAM_HAS_COMPLETED_RECIPES = KEYS.add3("team_has_completed_recipes", "Team %s has completed %s out of %s recipes"); public static final Component DONT_CHEAT = KEYS.add("dont_cheat", "Don't cheat!").withStyle(ChatFormatting.RED); - public static final Component HINT = MinigameTexts.KEYS.add("hint", "Click to show a hint, displaying the position of a random number of ingredients"); - public static final TranslationCollector.Fun1 HINTS_LEFT = MinigameTexts.KEYS.add1("hints_left", "You have %s hints left"); + public static final Component HINT = KEYS.add("hint", "Click to show a hint, displaying the position of a random number of ingredients"); + public static final TranslationCollector.Fun1 HINTS_LEFT = KEYS.add1("hints_left", "You have %s hints left"); } diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java index a72dfa02..64842c7c 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java @@ -2,18 +2,11 @@ import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; -import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import net.minecraft.Util; -import net.minecraft.core.RegistryAccess; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.RecipeHolder; -import net.minecraft.world.item.crafting.ShapedRecipe; -import net.minecraft.world.item.crafting.ShapelessRecipe; import java.util.List; @@ -25,20 +18,6 @@ public interface RecipeSelector { MapCodec getType(); - record SelectedRecipe(ResourceLocation id, Either recipe) { - public SelectedRecipe(RecipeHolder holder) { - this(holder.id(), holder.value() instanceof ShapedRecipe sr ? Either.left(sr) : Either.right((ShapelessRecipe) holder.value())); - } - - public ItemStack getResult(RegistryAccess access) { - return recipe.map(rp -> rp.getResultItem(access), rp -> rp.getResultItem(access)); - } - - public List decompose() { - return recipe.map(ShapedRecipe::getIngredients, ShapelessRecipe::getIngredients); - } - } - record FromList(List recipes) implements RecipeSelector { public static final MapCodec CODEC = ResourceLocation.CODEC.listOf().fieldOf("recipes") .xmap(FromList::new, FromList::recipes); diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/SelectedRecipe.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/SelectedRecipe.java new file mode 100644 index 00000000..2c65fa86 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/SelectedRecipe.java @@ -0,0 +1,31 @@ +package com.lovetropics.minigames.common.content.crafting_bee; + +import com.mojang.datafixers.util.Either; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.ShapelessRecipe; + +import java.util.List; + +public record SelectedRecipe(ResourceLocation id, Either recipe) { + public SelectedRecipe(RecipeHolder holder) { + this(holder.id(), holder.value() instanceof ShapedRecipe sr ? Either.left(sr) : Either.right((ShapelessRecipe) holder.value())); + } + + public SelectedRecipe(ResourceLocation id, RecipeManager manager) { + this(manager.byKey(id).orElseThrow()); + } + + public ItemStack getResult(RegistryAccess access) { + return recipe.map(rp -> rp.getResultItem(access), rp -> rp.getResultItem(access)); + } + + public List decompose() { + return recipe.map(ShapedRecipe::getIngredients, ShapelessRecipe::getIngredients); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java index 3f7f7989..b52b889a 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/FromRecipeDecomposer.java @@ -1,6 +1,6 @@ package com.lovetropics.minigames.common.content.crafting_bee.ingredient; -import com.lovetropics.minigames.common.content.crafting_bee.RecipeSelector; +import com.lovetropics.minigames.common.content.crafting_bee.SelectedRecipe; import com.mojang.serialization.MapCodec; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; @@ -38,7 +38,7 @@ public void prepareCache(ServerLevel level) { cache.clear(); for (ResourceLocation recipe : recipes) { - level.getServer().getRecipeManager().byKey(recipe).map(RecipeSelector.SelectedRecipe::new) + level.getServer().getRecipeManager().byKey(recipe).map(SelectedRecipe::new) .ifPresent(r -> cache.put( r.getResult(level.registryAccess()).getItem(), r.decompose() diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java index fd185747..4ad5f762 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java @@ -6,14 +6,20 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.UUIDUtil; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ExtraCodecs; import net.minecraft.world.item.ItemStack; import java.util.List; +import java.util.UUID; -public record CraftingBeeCrafts(List crafts) implements GameClientState { - public static final MapCodec CODEC = Craft.CODEC.listOf() - .fieldOf("crafts").xmap(CraftingBeeCrafts::new, CraftingBeeCrafts::crafts); +public record CraftingBeeCrafts(List crafts, UUID gameId, int allowedHints) implements GameClientState { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + Craft.CODEC.listOf().fieldOf("crafts").forGetter(CraftingBeeCrafts::crafts), + UUIDUtil.CODEC.fieldOf("gameId").forGetter(CraftingBeeCrafts::gameId), + Codec.INT.fieldOf("allowedHints").forGetter(CraftingBeeCrafts::allowedHints) + ).apply(in, CraftingBeeCrafts::new)); @Override public GameClientStateType getType() { diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java index e56a694a..68942e8f 100644 --- a/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java +++ b/src/main/java/com/lovetropics/minigames/mixin/client/HiddenRecipeBookMixin.java @@ -29,7 +29,7 @@ public class HiddenRecipeBookMixin { @Inject(method = "init", at = @At("HEAD")) private void hideBookIfOpen(CallbackInfo ci) { - if (ClientGameStateManager.getOrNull(GameClientStateTypes.HIDE_RECIPE_BOOK) != null && recipeBookComponent.isVisible()) { + if (ClientGameStateManager.getOrNull(GameClientStateTypes.HIDE_RECIPE_BOOK) != null) { Minecraft.getInstance().player.getRecipeBook().setBookSetting(RecipeBookType.CRAFTING, false, false); } } diff --git a/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/crafting_grid.png b/src/main/resources/assets/ltminigames/textures/gui/minigames/crafting_bee/crafting_grid.png new file mode 100644 index 0000000000000000000000000000000000000000..6bcd16977f63859f2f8d79eacc392d3b8c6b87b0 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SM3?%Ea%ijSh#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>0X`wF=H}+z-QEBH|6d&O{W3_fz$3Dlfr0NZ2s0kfUy%Y7)b@0746!(! z+`#Cj!{g@2A#f Date: Mon, 14 Oct 2024 13:12:05 +0300 Subject: [PATCH 11/15] Track wins correctly --- .../crafting_bee/CraftingBeeBehavior.java | 31 +++++++++++++++---- .../behaviour/VictoryPointsBehavior.java | 19 ++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java index 1dce916c..4ae575f2 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java @@ -16,6 +16,7 @@ import com.lovetropics.minigames.common.core.game.client_state.GameClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; import com.lovetropics.minigames.common.core.game.player.PlayerSet; +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; @@ -24,12 +25,18 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.BlockHitResult; import java.util.ArrayList; import java.util.Collection; @@ -55,6 +62,7 @@ public class CraftingBeeBehavior implements IGameBehavior { private IGamePhase game; private ListMultimap tasks; + private volatile boolean done; public CraftingBeeBehavior(List selectors, List decomposers, int allowedHints) { this.selectors = selectors; @@ -72,8 +80,8 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio teams = game.instanceState().getOrThrow(TeamState.KEY); events.listen(GamePhaseEvents.START, this::start); - events.listen(GamePlayerEvents.CRAFT, this::onCraft); + events.listen(GamePlayerEvents.USE_BLOCK, this::useBlock); } private void start() { @@ -131,7 +139,7 @@ private Stream singleDecomposition(Ingredient ingredient) { private void onCraft(Player player, ItemStack crafted, Container container) { var team = teams.getTeamForPlayer(player); - if (team == null) return; + if (team == null || done) return; var teamTasks = tasks.get(team); var task = teamTasks.stream().filter(c -> ItemStack.isSameItemSameComponents(crafted, c.output)).findFirst().orElse(null); @@ -142,22 +150,33 @@ private void onCraft(Player player, ItemStack crafted, Container container) { sync(team); var completed = teamTasks.stream().filter(t -> t.done).count(); - var teamName = teams.getTeamByKey(team).config().styledName(); + var teamConfig = teams.getTeamByKey(team).config(); - game.allPlayers().sendMessage(CraftingBeeTexts.TEAM_HAS_COMPLETED_RECIPES.apply(teamName, completed, teamTasks.size())); + game.allPlayers().sendMessage(CraftingBeeTexts.TEAM_HAS_COMPLETED_RECIPES.apply(teamConfig.styledName(), completed, teamTasks.size())); if (completed == teamTasks.size()) { game.statistics().global().set(StatisticKey.WINNING_TEAM, team); - game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(teamName); + game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(teamConfig.name()); + game.invoker(GameLogicEvents.GAME_OVER).onGameOver(); + + done = true; game.allPlayers().forEach(ServerPlayer::closeContainer); - game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamName).withStyle(ChatFormatting.GREEN), true)); + game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamConfig.styledName()).withStyle(ChatFormatting.GREEN), true)); game.schedule(5, () -> game.requestStop(GameStopReason.finished())); } } + private InteractionResult useBlock(ServerPlayer player, ServerLevel world, BlockPos pos, InteractionHand hand, BlockHitResult traceResult) { + // don't allow players to use the crafting table after the game was won + if (world.getBlockState(pos).is(Blocks.CRAFTING_TABLE) && done) { + return InteractionResult.FAIL; + } + return InteractionResult.PASS; + } + private void sync(GameTeamKey team) { teams.getPlayersForTeam(team).forEach(this::sync); } diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java index 7a5cf952..bf5a2d29 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/VictoryPointsBehavior.java @@ -9,6 +9,9 @@ import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; +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; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -59,6 +62,15 @@ private void onWinTriggered(Component component) { if (Objects.equals(player.getDisplayName(), component)) { tryAddPoints(player, pointsPerGameWon); player.displayClientMessage(Component.literal("YOU WIN!!!! Victory points for team: " + getPoints(player)), false); + return; + } + } + + var teams = game.instanceState().getOrNull(TeamState.KEY); + if (teams == null) return; + for (final GameTeam team : teams) { + if (Objects.equals(team.config().name(), component)) { + tryAddPoints(team.key(), pointsPerGameWon); } } } @@ -70,6 +82,13 @@ private void tryAddPoints(final ServerPlayer player, final int points) { } } + private void tryAddPoints(final GameTeamKey team, final int points) { + final VictoryPointsGameState pointState = state(); + if (pointState != null) { + pointState.addPointsToTeam(team, points); + } + } + private int getPoints(final ServerPlayer player) { final VictoryPointsGameState gameState = state(); if (gameState != null) { From a78223c7fb1c1e32fd7946f01a63c3379e74f20e Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Mon, 14 Oct 2024 14:50:48 +0300 Subject: [PATCH 12/15] Add client state to invert controls and swap movement See https://github.com/LoveTropics/LTMinigames/issues/200 --- .../game/handler/GameCraftingBeeHandler.java | 4 +- .../crafting_bee/CraftingBeeBehavior.java | 9 ++--- .../client_state/GameClientStateTypes.java | 12 ++++-- ...java => CraftingBeeCraftsClientState.java} | 13 +++---- ...te.java => HideRecipeBookClientState.java} | 6 +-- .../instance/InvertControlsClientState.java | 20 ++++++++++ .../instance/SwapMovementClientState.java | 15 ++++++++ .../mixin/client/KeyboardInputMixin.java | 22 +++++++++++ .../mixin/client/MouseHandlerMixin.java | 37 +++++++++++++++++++ src/main/resources/ltminigames.mixins.json | 4 +- 10 files changed, 120 insertions(+), 22 deletions(-) rename src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/{CraftingBeeCrafts.java => CraftingBeeCraftsClientState.java} (78%) rename src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/{HideRecipeBookState.java => HideRecipeBookClientState.java} (67%) create mode 100644 src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/InvertControlsClientState.java create mode 100644 src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/SwapMovementClientState.java create mode 100644 src/main/java/com/lovetropics/minigames/mixin/client/KeyboardInputMixin.java create mode 100644 src/main/java/com/lovetropics/minigames/mixin/client/MouseHandlerMixin.java diff --git a/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java index 0bc3d57c..17ae50e2 100644 --- a/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java +++ b/src/main/java/com/lovetropics/minigames/client/game/handler/GameCraftingBeeHandler.java @@ -5,7 +5,7 @@ import com.lovetropics.minigames.common.content.crafting_bee.CraftingBeeTexts; import com.lovetropics.minigames.common.content.crafting_bee.SelectedRecipe; import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; -import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCraftsClientState; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.platform.Lighting; import com.mojang.blaze3d.systems.RenderSystem; @@ -188,7 +188,7 @@ protected void updateWidgetNarration(NarrationElementOutput narrationElementOutp } @Nullable - private static CraftingBeeCrafts getState() { + private static CraftingBeeCraftsClientState getState() { return ClientGameStateManager.getOrNull(GameClientStateTypes.CRAFTING_BEE_CRAFTS); } diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java index 4ae575f2..8e7da80f 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java @@ -14,9 +14,8 @@ import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; import com.lovetropics.minigames.common.core.game.client_state.GameClientState; -import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCraftsClientState; import com.lovetropics.minigames.common.core.game.player.PlayerSet; -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; @@ -183,7 +182,7 @@ private void sync(GameTeamKey team) { private void sync(Player player) { if (player instanceof ServerPlayer sp) { - GameClientState.sendToPlayer(new CraftingBeeCrafts(tasks.get(teams.getTeamForPlayer(player)).stream().map(CraftingTask::toCraft).toList(), + GameClientState.sendToPlayer(new CraftingBeeCraftsClientState(tasks.get(teams.getTeamForPlayer(player)).stream().map(CraftingTask::toCraft).toList(), game.gameUuid(), allowedHints), sp); } } @@ -203,8 +202,8 @@ private CraftingTask(ItemStack output, SelectedRecipe recipe) { this.recipe = recipe; } - public CraftingBeeCrafts.Craft toCraft() { - return new CraftingBeeCrafts.Craft(output, recipe.id(), done); + public CraftingBeeCraftsClientState.Craft toCraft() { + return new CraftingBeeCraftsClientState.Craft(output, recipe.id(), done); } } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java index 61b2693c..58bd0a84 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/GameClientStateTypes.java @@ -2,16 +2,18 @@ import com.lovetropics.minigames.LoveTropics; import com.lovetropics.minigames.common.core.game.client_state.instance.BeaconClientState; -import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCrafts; +import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCraftsClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.FogClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.GlowTeamMembersState; import com.lovetropics.minigames.common.core.game.client_state.instance.HealthTagClientState; -import com.lovetropics.minigames.common.core.game.client_state.instance.HideRecipeBookState; +import com.lovetropics.minigames.common.core.game.client_state.instance.HideRecipeBookClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.InvertControlsClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.PointTagClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.ReplaceTexturesClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.ResourcePackClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.SidebarClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.SpectatingClientState; +import com.lovetropics.minigames.common.core.game.client_state.instance.SwapMovementClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.TeamMembersClientState; import com.lovetropics.minigames.common.core.game.client_state.instance.TimeInterpolationClientState; import com.lovetropics.minigames.common.util.registry.GameClientTweakEntry; @@ -45,8 +47,10 @@ public final class GameClientStateTypes { public static final GameClientTweakEntry TEAM_MEMBERS = register("team_members", TeamMembersClientState.CODEC); public static final GameClientTweakEntry GLOW_TEAM_MEMBERS = register("glow_team_members", MapCodec.unit(GlowTeamMembersState.INSTANCE)); public static final GameClientTweakEntry POINT_TAGS = register("point_tags", PointTagClientState.CODEC); - public static final GameClientTweakEntry HIDE_RECIPE_BOOK = register("hide_recipe_book", HideRecipeBookState.CODEC); - public static final GameClientTweakEntry CRAFTING_BEE_CRAFTS = register("crafting_bee_crafts", CraftingBeeCrafts.CODEC); + public static final GameClientTweakEntry HIDE_RECIPE_BOOK = register("hide_recipe_book", HideRecipeBookClientState.CODEC); + public static final GameClientTweakEntry CRAFTING_BEE_CRAFTS = register("crafting_bee_crafts", CraftingBeeCraftsClientState.CODEC); + public static final GameClientTweakEntry INVERT_CONTROLS = register("invert_controls", InvertControlsClientState.CODEC); + public static final GameClientTweakEntry SWAP_MOVEMENT = register("swap_movement", SwapMovementClientState.CODEC); public static GameClientTweakEntry register(final String name, final MapCodec codec) { return REGISTRATE.object(name) diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCraftsClientState.java similarity index 78% rename from src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java rename to src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCraftsClientState.java index 4ad5f762..c5369008 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCrafts.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/CraftingBeeCraftsClientState.java @@ -8,18 +8,17 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.UUIDUtil; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.ExtraCodecs; import net.minecraft.world.item.ItemStack; import java.util.List; import java.util.UUID; -public record CraftingBeeCrafts(List crafts, UUID gameId, int allowedHints) implements GameClientState { - public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( - Craft.CODEC.listOf().fieldOf("crafts").forGetter(CraftingBeeCrafts::crafts), - UUIDUtil.CODEC.fieldOf("gameId").forGetter(CraftingBeeCrafts::gameId), - Codec.INT.fieldOf("allowedHints").forGetter(CraftingBeeCrafts::allowedHints) - ).apply(in, CraftingBeeCrafts::new)); +public record CraftingBeeCraftsClientState(List crafts, UUID gameId, int allowedHints) implements GameClientState { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + Craft.CODEC.listOf().fieldOf("crafts").forGetter(CraftingBeeCraftsClientState::crafts), + UUIDUtil.CODEC.fieldOf("gameId").forGetter(CraftingBeeCraftsClientState::gameId), + Codec.INT.fieldOf("allowedHints").forGetter(CraftingBeeCraftsClientState::allowedHints) + ).apply(in, CraftingBeeCraftsClientState::new)); @Override public GameClientStateType getType() { diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookClientState.java similarity index 67% rename from src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookState.java rename to src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookClientState.java index 4ab26155..16d5959f 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookState.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/HideRecipeBookClientState.java @@ -7,9 +7,9 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentSerialization; -public record HideRecipeBookState(Component message) implements GameClientState { - public static final MapCodec CODEC = ComponentSerialization.CODEC - .fieldOf("message").xmap(HideRecipeBookState::new, HideRecipeBookState::message); +public record HideRecipeBookClientState(Component message) implements GameClientState { + public static final MapCodec CODEC = ComponentSerialization.CODEC + .fieldOf("message").xmap(HideRecipeBookClientState::new, HideRecipeBookClientState::message); @Override public GameClientStateType getType() { diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/InvertControlsClientState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/InvertControlsClientState.java new file mode 100644 index 00000000..1efe5574 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/InvertControlsClientState.java @@ -0,0 +1,20 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +public record InvertControlsClientState(boolean xAxis, boolean yAxis) implements GameClientState { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + Codec.BOOL.optionalFieldOf("x_axis", false).forGetter(InvertControlsClientState::xAxis), + Codec.BOOL.optionalFieldOf("y_axis", true).forGetter(InvertControlsClientState::yAxis) + ).apply(in, InvertControlsClientState::new)); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.INVERT_CONTROLS.get(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/SwapMovementClientState.java b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/SwapMovementClientState.java new file mode 100644 index 00000000..8947e094 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/core/game/client_state/instance/SwapMovementClientState.java @@ -0,0 +1,15 @@ +package com.lovetropics.minigames.common.core.game.client_state.instance; + +import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateType; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import com.mojang.serialization.MapCodec; + +public record SwapMovementClientState() implements GameClientState { + public static final MapCodec CODEC = MapCodec.unit(SwapMovementClientState::new); + + @Override + public GameClientStateType getType() { + return GameClientStateTypes.SWAP_MOVEMENT.get(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/KeyboardInputMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/KeyboardInputMixin.java new file mode 100644 index 00000000..2bba29d5 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/client/KeyboardInputMixin.java @@ -0,0 +1,22 @@ +package com.lovetropics.minigames.mixin.client; + +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import net.minecraft.client.player.Input; +import net.minecraft.client.player.KeyboardInput; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(KeyboardInput.class) +public class KeyboardInputMixin extends Input { + @Inject(method = "tick", at = @At("TAIL")) + private void respectSwappedMovement(boolean isSneaking, float sneakingSpeedMultiplier, CallbackInfo ci) { + if (ClientGameStateManager.getOrNull(GameClientStateTypes.SWAP_MOVEMENT) != null) { + float oldLeft = leftImpulse; + leftImpulse = forwardImpulse; + forwardImpulse = oldLeft; + } + } +} diff --git a/src/main/java/com/lovetropics/minigames/mixin/client/MouseHandlerMixin.java b/src/main/java/com/lovetropics/minigames/mixin/client/MouseHandlerMixin.java new file mode 100644 index 00000000..81eb7d8f --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/client/MouseHandlerMixin.java @@ -0,0 +1,37 @@ +package com.lovetropics.minigames.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.lovetropics.minigames.client.game.ClientGameStateManager; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; +import net.minecraft.client.Minecraft; +import net.minecraft.client.MouseHandler; +import net.minecraft.client.player.LocalPlayer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MouseHandler.class) +public class MouseHandlerMixin { + @Shadow + @Final + private Minecraft minecraft; + + @WrapOperation(method = "turnPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;turn(DD)V")) + private void respectControlModificationState(LocalPlayer player, double dx, double dy, Operation original) { + var state = ClientGameStateManager.getOrNull(GameClientStateTypes.INVERT_CONTROLS); + if (state != null) { + if (state.xAxis()) { + dx = dx * -1; + } + + // Avoid allowing the bypass of the setting by modifying the options + if (state.yAxis() && !minecraft.options.invertYMouse().get()) { + dy = dy * -1; + } + } + + original.call(player, dx, dy); + } +} diff --git a/src/main/resources/ltminigames.mixins.json b/src/main/resources/ltminigames.mixins.json index 4397675e..be228bab 100644 --- a/src/main/resources/ltminigames.mixins.json +++ b/src/main/resources/ltminigames.mixins.json @@ -29,7 +29,9 @@ "client.MinecraftMixin", "client.PlayerTabOverlayGuiMixin", "client.PoseStackAccessor", - "client.HiddenRecipeBookMixin" + "client.HiddenRecipeBookMixin", + "client.MouseHandlerMixin", + "client.KeyboardInputMixin" ], "injectors": { "defaultRequire": 1 From 826f4992faa0533aed71882ab5f09e00583d7a11 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Fri, 18 Oct 2024 22:55:32 +0300 Subject: [PATCH 13/15] Connect four --- .../assets/ltminigames/lang/en_ud.json | 3 + .../assets/ltminigames/lang/en_us.json | 3 + .../lovetropics/minigames/LoveTropics.java | 4 + .../common/content/MinigameTexts.java | 1 + .../common/content/connect4/ConnectFour.java | 16 ++ .../content/connect4/ConnectFourBehavior.java | 235 ++++++++++++++++++ .../content/connect4/ConnectFourTexts.java | 13 + .../game/behavior/event/GameWorldEvents.java | 11 + .../core/game/state/team/TeamState.java | 6 + .../minigames/common/util/SequentialList.java | 29 +++ .../mixin/FallingBlockEntityMixin.java | 33 +++ src/main/resources/ltminigames.mixins.json | 1 + 12 files changed, 355 insertions(+) create mode 100644 src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFour.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java create mode 100644 src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourTexts.java create mode 100644 src/main/java/com/lovetropics/minigames/common/util/SequentialList.java create mode 100644 src/main/java/com/lovetropics/minigames/mixin/FallingBlockEntityMixin.java diff --git a/src/generated/resources/assets/ltminigames/lang/en_ud.json b/src/generated/resources/assets/ltminigames/lang/en_ud.json index a71c70a2..ab153ef2 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_ud.json +++ b/src/generated/resources/assets/ltminigames/lang/en_ud.json @@ -167,6 +167,9 @@ "ltminigames.minigame.build_competition": "uoıʇıʇǝdɯoƆ pןınᗺ", "ltminigames.minigame.calamity": "ʎʇıɯɐןɐƆ", "ltminigames.minigame.chaos_block_party": "ʎʇɹɐԀ ʞɔoןᗺ soɐɥƆ", + "ltminigames.minigame.connect_four": "ɹnoℲ ʇɔǝuuoƆ", + "ltminigames.minigame.connect_four.teams_goes_next": "ʇxǝu sǝob %s ɯɐǝ⟘", + "ltminigames.minigame.connect_four.your_turn": "ʞɔoןq ɐ ǝɔɐןd oʇ uɹnʇ ɹnoʎ sı ʇI", "ltminigames.minigame.conservation_exploration": "uoıʇɐɹoןdxƎ uoıʇɐʌɹǝsuoƆ", "ltminigames.minigame.crafting_bee.dont_cheat": "¡ʇɐǝɥɔ ʇ,uoᗡ", "ltminigames.minigame.crafting_bee.hint": "sʇuǝıpǝɹbuı ɟo ɹǝqɯnu ɯopuɐɹ ɐ ɟo uoıʇısod ǝɥʇ buıʎɐןdsıp 'ʇuıɥ ɐ ʍoɥs oʇ ʞɔıןƆ", diff --git a/src/generated/resources/assets/ltminigames/lang/en_us.json b/src/generated/resources/assets/ltminigames/lang/en_us.json index 1d748da8..01919e3f 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_us.json +++ b/src/generated/resources/assets/ltminigames/lang/en_us.json @@ -167,6 +167,9 @@ "ltminigames.minigame.build_competition": "Build Competition", "ltminigames.minigame.calamity": "Calamity", "ltminigames.minigame.chaos_block_party": "Chaos Block Party", + "ltminigames.minigame.connect_four": "Connect Four", + "ltminigames.minigame.connect_four.teams_goes_next": "Team %s goes next", + "ltminigames.minigame.connect_four.your_turn": "It is your turn to place a block", "ltminigames.minigame.conservation_exploration": "Conservation Exploration", "ltminigames.minigame.crafting_bee.dont_cheat": "Don't cheat!", "ltminigames.minigame.crafting_bee.hint": "Click to show a hint, displaying the position of a random number of ingredients", diff --git a/src/main/java/com/lovetropics/minigames/LoveTropics.java b/src/main/java/com/lovetropics/minigames/LoveTropics.java index 0b7e01f2..a9fc88ba 100644 --- a/src/main/java/com/lovetropics/minigames/LoveTropics.java +++ b/src/main/java/com/lovetropics/minigames/LoveTropics.java @@ -15,6 +15,8 @@ import com.lovetropics.minigames.common.content.block_party.BlockParty; import com.lovetropics.minigames.common.content.block_party.BlockPartyTexts; import com.lovetropics.minigames.common.content.build_competition.BuildCompetition; +import com.lovetropics.minigames.common.content.connect4.ConnectFour; +import com.lovetropics.minigames.common.content.connect4.ConnectFourTexts; import com.lovetropics.minigames.common.content.crafting_bee.CraftingBee; import com.lovetropics.minigames.common.content.crafting_bee.CraftingBeeTexts; import com.lovetropics.minigames.common.content.hide_and_seek.HideAndSeek; @@ -120,6 +122,7 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { BiodiversityBlitzTexts.collectTranslations(consumer); BlockPartyTexts.KEYS.forEach(consumer); CraftingBeeTexts.KEYS.forEach(consumer); + ConnectFourTexts.KEYS.forEach(consumer); SurviveTheTideTexts.KEYS.forEach(consumer); TrashDiveTexts.KEYS.forEach(consumer); TurtleRaceTexts.KEYS.forEach(consumer); @@ -149,6 +152,7 @@ public LoveTropics(IEventBus modBus, ModContainer modContainer) { TrashDive.init(); BlockParty.init(); CraftingBee.init(); + ConnectFour.init(); TurtleRace.init(); Qottott.init(); Spleef.init(); diff --git a/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java b/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java index 35f71235..70edb0b7 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java +++ b/src/main/java/com/lovetropics/minigames/common/content/MinigameTexts.java @@ -34,6 +34,7 @@ public final class MinigameTexts { public static final Component CHAOS_BLOCK_PARTY = KEYS.add("chaos_block_party", "Chaos Block Party"); public static final Component LEVITATION = KEYS.add("levitation", "Levitation"); public static final Component QOTTOTT = KEYS.add("qottott", "Qottott"); + public static final Component CONNECT_FOUR = KEYS.add("connect_four", "Connect Four"); // TODO: These should move into SurviveTheTideTexts public static final Component[] SURVIVE_THE_TIDE_INTRO = { diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFour.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFour.java new file mode 100644 index 00000000..60b7f15d --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFour.java @@ -0,0 +1,16 @@ +package com.lovetropics.minigames.common.content.connect4; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.util.registry.GameBehaviorEntry; +import com.lovetropics.minigames.common.util.registry.LoveTropicsRegistrate; + +public class ConnectFour { + private static final LoveTropicsRegistrate REGISTRATE = LoveTropics.registrate(); + + public static final GameBehaviorEntry CONNECT_FOUR = REGISTRATE.object("connect_four") + .behavior(ConnectFourBehavior.CODEC) + .register(); + + public static void init() { + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java new file mode 100644 index 00000000..ab0f95d3 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java @@ -0,0 +1,235 @@ +package com.lovetropics.minigames.common.content.connect4; + +import com.lovetropics.lib.BlockBox; +import com.lovetropics.minigames.common.content.MinigameTexts; +import com.lovetropics.minigames.common.core.game.GameException; +import com.lovetropics.minigames.common.core.game.GameStopReason; +import com.lovetropics.minigames.common.core.game.IGamePhase; +import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; +import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GameLogicEvents; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; +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.GameTeamKey; +import com.lovetropics.minigames.common.core.game.state.team.TeamState; +import com.lovetropics.minigames.common.util.SequentialList; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +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.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +public class ConnectFourBehavior implements IGameBehavior { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + Codec.unboundedMap(GameTeamKey.CODEC, GameBlock.CODEC.codec()).fieldOf("team_blocks").forGetter(c -> c.teamBlocks), + Codec.STRING.fieldOf("placing_region").forGetter(c -> c.placingRegionKey), + BlockState.CODEC.fieldOf("separator").forGetter(c -> c.separator), + BlockState.CODEC.fieldOf("blocker").forGetter(c -> c.blocker) + ).apply(in, ConnectFourBehavior::new)); + + private final Map teamBlocks; + private final String placingRegionKey; + private final BlockState separator; + private final BlockState blocker; + + public ConnectFourBehavior(Map teamBlocks, String placingRegionKey, BlockState separator, BlockState blocker) { + this.teamBlocks = teamBlocks; + this.placingRegionKey = placingRegionKey; + this.separator = separator; + this.blocker = blocker; + } + + private IGamePhase game; + + @Nullable + private PendingGate pendingGate; + + private SequentialList playingTeams; + + private TeamState teams; + private BlockBox placingRegion; + + private GameTeamKey[][] pieces; + + @Override + public void register(IGamePhase game, EventRegistrar events) throws GameException { + this.game = game; + placingRegion = game.mapRegions().getOrThrow(placingRegionKey); + teams = game.instanceState().getOrThrow(TeamState.KEY); + + pieces = new GameTeamKey[7][6]; + + events.listen(GamePhaseEvents.START, this::onStart); + + events.listen(GamePlayerEvents.PLACE_BLOCK, this::onPlaceBlock); + events.listen(GamePlayerEvents.BREAK_BLOCK, (player, pos, state, hand) -> player.isCreative() ? InteractionResult.PASS : InteractionResult.FAIL); + + events.listen(GameWorldEvents.BLOCK_LANDED, this::onBlockLanded); + } + + private void onStart() { + var teams = new ArrayList(this.teams.getTeamKeys().size()); + this.teams.getTeamKeys().forEach(key -> { + var players = this.teams.getPlayersForTeam(key).stream().map(PlayerKey::from).toList(); + if (!players.isEmpty()) { + teams.add(new PlayingTeam(key, new SequentialList<>(players, -1))); + } + }); + Collections.shuffle(teams); + playingTeams = new SequentialList<>(teams, -1); + + nextPlayer(); + } + + private InteractionResult onPlaceBlock(ServerPlayer player, BlockPos pos, BlockState placed, BlockState placedOn) { + if (player.isCreative()) return InteractionResult.PASS; + + if (!Objects.equals(playingTeams.current().players.current(), PlayerKey.from(player)) || !placingRegion.contains(pos)) + return InteractionResult.FAIL; + + var expected = teamBlocks.get(playingTeams.current().key).powder; + if (expected != placed.getBlock()) return InteractionResult.FAIL; + + var below = pos.below(); + pendingGate = new PendingGate(below, player.level().getBlockState(below)); + player.level().setBlock(below, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + + return InteractionResult.PASS; + } + + private void onBlockLanded(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + var expected = teamBlocks.get(playingTeams.current().key); + + if (!state.is(expected.powder)) return; + + level.setBlock(pos, expected.solid.defaultBlockState(), Block.UPDATE_ALL); + + if (pendingGate != null) { + level.setBlock(pendingGate.gatePosition(), pendingGate.gate(), Block.UPDATE_ALL); + pendingGate = null; + } + + level.setBlock(pos.above(), separator, Block.UPDATE_ALL); + // Separator above, and gate above that + if (placingRegion.contains(pos.getX(), pos.getY() + 3, pos.getZ())) { + level.setBlock(pos.above(3), blocker, Block.UPDATE_ALL); + } + + int x = (pos.getX() - placingRegion.min().getX()) / 2; + var column = pieces[x]; + int y; + for (y = 0; y < column.length; y++) { + if (column[y] == null) { + column[y] = playingTeams.current().key(); + break; + } + } + + var team = playingTeams.current().key(); + + game.allPlayers().getPlayerBy(playingTeams.current().players().current()).setGlowingTag(false); + + if (checkWin(x, y, team)) { + game.statistics().global().set(StatisticKey.WINNING_TEAM, team); + var teamConfig = teams.getTeamByKey(team).config(); + game.invoker(GameLogicEvents.WIN_TRIGGERED).onWinTriggered(teamConfig.styledName()); + game.invoker(GameLogicEvents.GAME_OVER).onGameOver(); + + game.allPlayers().forEach(ServerPlayer::closeContainer); + + game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamConfig.styledName()).withStyle(ChatFormatting.GREEN), true)); + game.schedule(5, () -> game.requestStop(GameStopReason.finished())); + } else { + nextPlayer(); + } + } + + private void nextPlayer() { + var nextTeam = playingTeams.next(); + var nextPlayer = nextTeam.players().next(); + + game.allPlayers().sendMessage(ConnectFourTexts.TEAM_GOES_NEXT.apply(teams.getTeamByKey(nextTeam.key).config().styledName()), false); + + var player = game.allPlayers().getPlayerBy(nextPlayer); + player.addItem(teamBlocks.get(nextTeam.key).powder.asItem().getDefaultInstance()); + + player.setGlowingTag(true); + player.displayClientMessage(ConnectFourTexts.IT_IS_YOUR_TURN.copy().withStyle(ChatFormatting.GOLD), true); + } + + private boolean checkWin(int x, int y, GameTeamKey team) { + if (checkLine(x, y, 0, -1, team)) { // vertical + return true; + } + + for (int offset = 0; offset < 4; ++offset) { + if (checkLine(x - 3 + offset, y, 1, 0, team)) { // horizontal + return true; + } + + if (checkLine(x - 3 + offset, y + 3 - offset, 1, -1, team)) { // leading diagonal + return true; + } + + if (checkLine(x - 3 + offset, y - 3 + offset, 1, 1, team)) { // trailing diagonal + return true; + } + } + + return false; + } + + + private boolean checkLine(int xs, int ys, int dx, int dy, GameTeamKey team) { + for (int i = 0; i < 4; i++) { + int x = xs + (dx * i); + int y = ys + (dy * 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]) { + return false; + } + } + + return true; + } + + private record GameBlock(Block powder, Block solid) { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( + BuiltInRegistries.BLOCK.byNameCodec().fieldOf("powder").forGetter(GameBlock::powder), + BuiltInRegistries.BLOCK.byNameCodec().fieldOf("solid").forGetter(GameBlock::solid) + ).apply(in, GameBlock::new)); + } + + private record PendingGate(BlockPos gatePosition, BlockState gate) { + } + + private record PlayingTeam(GameTeamKey key, SequentialList players) { + + } +} diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourTexts.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourTexts.java new file mode 100644 index 00000000..9588c35b --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourTexts.java @@ -0,0 +1,13 @@ +package com.lovetropics.minigames.common.content.connect4; + +import com.lovetropics.minigames.LoveTropics; +import com.lovetropics.minigames.common.core.game.util.TranslationCollector; +import net.minecraft.network.chat.Component; + +public class ConnectFourTexts { + public static final TranslationCollector KEYS = new TranslationCollector(LoveTropics.ID + ".minigame.connect_four."); + + public static final Component IT_IS_YOUR_TURN = KEYS.add("your_turn", "It is your turn to place a block"); + + public static final TranslationCollector.Fun1 TEAM_GOES_NEXT = KEYS.add1("teams_goes_next", "Team %s goes next"); +} diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java index 0ab66d34..42fb2d1a 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/event/GameWorldEvents.java @@ -6,6 +6,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.level.Explosion; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; import javax.annotation.Nullable; @@ -42,6 +43,12 @@ public final class GameWorldEvents { } }); + public static final GameEventType BLOCK_LANDED = GameEventType.create(BlockLanded.class, listeners -> (level, pos, state) -> { + for (var listener : listeners) { + listener.onBlockLanded(level, pos, state); + } + }); + private GameWorldEvents() { } @@ -60,4 +67,8 @@ public interface SaplingGrow { public interface SetWeather { void onSetWeather(@Nullable WeatherEvent lastEvent, @Nullable WeatherEvent event); } + + public interface BlockLanded { + void onBlockLanded(Level level, BlockPos pos, BlockState state); + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java b/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java index d67bbc03..74a8b19d 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/state/team/TeamState.java @@ -10,6 +10,7 @@ import com.lovetropics.minigames.common.core.game.player.PlayerSet; import com.lovetropics.minigames.common.core.game.state.GameStateKey; import com.lovetropics.minigames.common.core.game.state.IGameState; +import com.lovetropics.minigames.common.core.game.state.statistics.PlayerKey; import com.lovetropics.minigames.common.core.game.util.TeamAllocator; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; @@ -101,6 +102,11 @@ public GameTeamKey getTeamForPlayer(Player player) { return getTeamForPlayer(player.getUUID()); } + @Nullable + public GameTeamKey getTeamForPlayer(PlayerKey player) { + return getTeamForPlayer(player.id()); + } + @Nullable public GameTeamKey getTeamForPlayer(UUID playerId) { for (Map.Entry entry : Object2ObjectMaps.fastIterable(playersByKey)) { diff --git a/src/main/java/com/lovetropics/minigames/common/util/SequentialList.java b/src/main/java/com/lovetropics/minigames/common/util/SequentialList.java new file mode 100644 index 00000000..5fa51b7b --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/common/util/SequentialList.java @@ -0,0 +1,29 @@ +package com.lovetropics.minigames.common.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class SequentialList { + private final List list; + private int index; + + public SequentialList(List list, int index) { + this.list = list; + this.index = index; + } + + public T current() { + return list.get(index); + } + + public T next() { + if (index >= list.size() - 1) { + index = 0; + } else { + index++; + } + + return current(); + } +} diff --git a/src/main/java/com/lovetropics/minigames/mixin/FallingBlockEntityMixin.java b/src/main/java/com/lovetropics/minigames/mixin/FallingBlockEntityMixin.java new file mode 100644 index 00000000..dc91adf8 --- /dev/null +++ b/src/main/java/com/lovetropics/minigames/mixin/FallingBlockEntityMixin.java @@ -0,0 +1,33 @@ +package com.lovetropics.minigames.mixin; + +import com.lovetropics.minigames.common.core.game.IGameManager; +import com.lovetropics.minigames.common.core.game.behavior.event.GameWorldEvents; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FallingBlockEntity.class) +public abstract class FallingBlockEntityMixin extends Entity { + + public FallingBlockEntityMixin(EntityType entityType, Level level) { + super(entityType, level); + } + + @Shadow + private BlockState blockState; + + @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); + } + } +} diff --git a/src/main/resources/ltminigames.mixins.json b/src/main/resources/ltminigames.mixins.json index be228bab..001bd71d 100644 --- a/src/main/resources/ltminigames.mixins.json +++ b/src/main/resources/ltminigames.mixins.json @@ -13,6 +13,7 @@ "SimpleRegistryMixin", "TagManagerAccessor", "WorldGenSettingsMixin", + "FallingBlockEntityMixin", "chat.ServerGamePacketListenerImplMixin", "disguise.LivingEntityMixin", "gametest.GameTestHelperAccess", From 239cd2fb896eb30c25c06d7b2938e7e6631117c7 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Fri, 18 Oct 2024 22:58:11 +0300 Subject: [PATCH 14/15] Track draw --- .../common/content/connect4/ConnectFourBehavior.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java index ab0f95d3..69cf8368 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java @@ -66,6 +66,7 @@ public ConnectFourBehavior(Map teamBlocks, String placin private BlockBox placingRegion; private GameTeamKey[][] pieces; + private int placedPieces; @Override public void register(IGamePhase game, EventRegistrar events) throws GameException { @@ -74,6 +75,7 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio teams = game.instanceState().getOrThrow(TeamState.KEY); pieces = new GameTeamKey[7][6]; + placedPieces = 0; events.listen(GamePhaseEvents.START, this::onStart); @@ -142,6 +144,7 @@ private void onBlockLanded(Level level, BlockPos pos, BlockState state) { break; } } + placedPieces++; var team = playingTeams.current().key(); @@ -158,7 +161,14 @@ private void onBlockLanded(Level level, BlockPos pos, BlockState state) { game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamConfig.styledName()).withStyle(ChatFormatting.GREEN), true)); game.schedule(5, () -> game.requestStop(GameStopReason.finished())); } else { - nextPlayer(); + if (placedPieces == 7 * 6) { + game.invoker(GameLogicEvents.GAME_OVER).onGameOver(); + + game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.NOBODY_WON, true)); + game.schedule(5, () -> game.requestStop(GameStopReason.finished())); + } else { + nextPlayer(); + } } } From 82e9e591b7d648c3fd5e98bc73900ecdf0fbf3b9 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sat, 19 Oct 2024 19:24:58 +0300 Subject: [PATCH 15/15] More work on crafting bee and connect four --- .../content/connect4/ConnectFourBehavior.java | 29 +++++--- .../crafting_bee/CraftingBeeBehavior.java | 6 +- .../content/crafting_bee/RecipeSelector.java | 66 +++++++++++++++++-- .../ingredient/SimpleTagToItemDecomposer.java | 2 +- .../instances/SetGameClientStateBehavior.java | 2 + 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java index 69cf8368..b8255e2a 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/connect4/ConnectFourBehavior.java @@ -1,6 +1,7 @@ package com.lovetropics.minigames.common.content.connect4; import com.lovetropics.lib.BlockBox; +import com.lovetropics.lib.codec.MoreCodecs; import com.lovetropics.minigames.common.content.MinigameTexts; import com.lovetropics.minigames.common.core.game.GameException; import com.lovetropics.minigames.common.core.game.GameStopReason; @@ -39,8 +40,11 @@ public class ConnectFourBehavior implements IGameBehavior { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( Codec.unboundedMap(GameTeamKey.CODEC, GameBlock.CODEC.codec()).fieldOf("team_blocks").forGetter(c -> c.teamBlocks), Codec.STRING.fieldOf("placing_region").forGetter(c -> c.placingRegionKey), - BlockState.CODEC.fieldOf("separator").forGetter(c -> c.separator), - BlockState.CODEC.fieldOf("blocker").forGetter(c -> c.blocker) + MoreCodecs.BLOCK_STATE.fieldOf("separator").forGetter(c -> c.separator), + MoreCodecs.BLOCK_STATE.fieldOf("blocker").forGetter(c -> c.blocker), + Codec.INT.fieldOf("grid_width").forGetter(c -> c.width), + Codec.INT.fieldOf("grid_height").forGetter(c -> c.height), + Codec.INT.optionalFieldOf("connect", 4).forGetter(c -> c.connectAmount) ).apply(in, ConnectFourBehavior::new)); private final Map teamBlocks; @@ -48,11 +52,16 @@ public class ConnectFourBehavior implements IGameBehavior { private final BlockState separator; private final BlockState blocker; - public ConnectFourBehavior(Map teamBlocks, String placingRegionKey, BlockState separator, BlockState blocker) { + private final int width, height, connectAmount; + + public ConnectFourBehavior(Map teamBlocks, String placingRegionKey, BlockState separator, BlockState blocker, int width, int height, int connectAmount) { this.teamBlocks = teamBlocks; this.placingRegionKey = placingRegionKey; this.separator = separator; this.blocker = blocker; + this.width = width; + this.height = height; + this.connectAmount = connectAmount; } private IGamePhase game; @@ -74,7 +83,7 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio placingRegion = game.mapRegions().getOrThrow(placingRegionKey); teams = game.instanceState().getOrThrow(TeamState.KEY); - pieces = new GameTeamKey[7][6]; + pieces = new GameTeamKey[width][height]; placedPieces = 0; events.listen(GamePhaseEvents.START, this::onStart); @@ -161,7 +170,7 @@ private void onBlockLanded(Level level, BlockPos pos, BlockState state) { game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.TEAM_WON.apply(teamConfig.styledName()).withStyle(ChatFormatting.GREEN), true)); game.schedule(5, () -> game.requestStop(GameStopReason.finished())); } else { - if (placedPieces == 7 * 6) { + if (placedPieces == width * height) { game.invoker(GameLogicEvents.GAME_OVER).onGameOver(); game.schedule(1.5f, () -> game.allPlayers().sendMessage(MinigameTexts.NOBODY_WON, true)); @@ -190,16 +199,16 @@ private boolean checkWin(int x, int y, GameTeamKey team) { return true; } - for (int offset = 0; offset < 4; ++offset) { - if (checkLine(x - 3 + offset, y, 1, 0, team)) { // horizontal + for (int offset = 0; offset < connectAmount; offset++) { + if (checkLine(x - (connectAmount - 1) + offset, y, 1, 0, team)) { // horizontal return true; } - if (checkLine(x - 3 + offset, y + 3 - offset, 1, -1, team)) { // leading diagonal + if (checkLine(x - (connectAmount + 1) + offset, y + (connectAmount - 1) - offset, 1, -1, team)) { // leading diagonal return true; } - if (checkLine(x - 3 + offset, y - 3 + offset, 1, 1, team)) { // trailing diagonal + if (checkLine(x - (connectAmount + 1) + offset, y - (connectAmount + 1) + offset, 1, 1, team)) { // trailing diagonal return true; } } @@ -209,7 +218,7 @@ private boolean checkWin(int x, int y, GameTeamKey team) { private boolean checkLine(int xs, int ys, int dx, int dy, GameTeamKey team) { - for (int i = 0; i < 4; i++) { + for (int i = 0; i < connectAmount; i++) { int x = xs + (dx * i); int y = ys + (dy * i); diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java index 8e7da80f..1c955aac 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/CraftingBeeBehavior.java @@ -14,6 +14,7 @@ import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; import com.lovetropics.minigames.common.core.game.client_state.GameClientState; +import com.lovetropics.minigames.common.core.game.client_state.GameClientStateTypes; import com.lovetropics.minigames.common.core.game.client_state.instance.CraftingBeeCraftsClientState; import com.lovetropics.minigames.common.core.game.player.PlayerSet; import com.lovetropics.minigames.common.core.game.state.statistics.StatisticKey; @@ -48,7 +49,7 @@ public class CraftingBeeBehavior implements IGameBehavior { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(in -> in.group( - RecipeSelector.CODEC.codec().listOf().fieldOf("selectors").forGetter(c -> c.selectors), + RecipeSelector.CODEC.listOf().fieldOf("selectors").forGetter(c -> c.selectors), IngredientDecomposer.CODEC.codec().listOf().fieldOf("decomposers").forGetter(c -> c.decomposers), Codec.INT.optionalFieldOf("hints_per_player", 3).forGetter(c -> c.allowedHints) ).apply(in, CraftingBeeBehavior::new)); @@ -81,6 +82,9 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio events.listen(GamePhaseEvents.START, this::start); events.listen(GamePlayerEvents.CRAFT, this::onCraft); events.listen(GamePlayerEvents.USE_BLOCK, this::useBlock); + + events.listen(GamePhaseEvents.STOP, reason -> GameClientState.removeFromPlayers(GameClientStateTypes.CRAFTING_BEE_CRAFTS.get(), game.allPlayers())); + events.listen(GamePlayerEvents.REMOVE, player -> GameClientState.removeFromPlayer(GameClientStateTypes.CRAFTING_BEE_CRAFTS.get(), player)); } private void start() { diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java index 64842c7c..a611af8a 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/RecipeSelector.java @@ -2,17 +2,24 @@ import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; +import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import net.minecraft.Util; +import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; import java.util.List; +import java.util.Optional; public interface RecipeSelector { - BiMap> TYPES = ImmutableBiMap.of("from_list", FromList.CODEC); - MapCodec CODEC = Codec.STRING.dispatchMap(s -> TYPES.inverse().get(s.getType()), TYPES::get); + BiMap> TYPES = ImmutableBiMap.of("from_list", FromList.CODEC, "one_of", OneOf.CODEC, "from_item_tag", FromItemTag.CODEC); + Codec CODEC = Codec.STRING.dispatch(s -> TYPES.inverse().get(s.getType()), TYPES::get); SelectedRecipe select(ServerLevel level); @@ -24,9 +31,58 @@ record FromList(List recipes) implements RecipeSelector { @Override public SelectedRecipe select(ServerLevel level) { - var key = Util.getRandom(recipes, level.getRandom()); - var recipe = level.getRecipeManager().byKey(key).orElseThrow(() -> new NullPointerException("Recipe " + key + " doesn't exist")); - return new SelectedRecipe(recipe); + Optional> recipe = Optional.empty(); + while (recipe.isEmpty()) { + var key = Util.getRandom(recipes, level.getRandom()); + recipe = level.getRecipeManager().byKey(key); + if (recipe.isEmpty()) { + LogUtils.getLogger().error("Recipe '{}' doesn't exist", key); + } + } + return new SelectedRecipe(recipe.get()); + } + + @Override + public MapCodec getType() { + return CODEC; + } + } + + record OneOf(List selectors) implements RecipeSelector { + public static final MapCodec CODEC = MapCodec.assumeMapUnsafe(Codec.lazyInitialized(() -> RecipeSelector.CODEC.listOf().fieldOf("selectors") + .xmap(OneOf::new, OneOf::selectors).codec())); + @Override + public SelectedRecipe select(ServerLevel level) { + return Util.getRandom(selectors, level.getRandom()).select(level); + } + + @Override + public MapCodec getType() { + return CODEC; + } + } + + class FromItemTag implements RecipeSelector { + public static final MapCodec CODEC = TagKey.hashedCodec(Registries.ITEM).fieldOf("tag") + .xmap(FromItemTag::new, s -> s.tag); + + private final TagKey tag; + + private List cache; + + public FromItemTag(TagKey tag) { + this.tag = tag; + } + + @Override + public SelectedRecipe select(ServerLevel level) { + if (cache == null) { + cache = level.getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING) + .stream().filter(h -> h.value().getResultItem(level.registryAccess()).is(tag) && h.id().getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) + .map(SelectedRecipe::new) + .toList(); + } + return Util.getRandom(cache, level.getRandom()); } @Override diff --git a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java index ac48ed9d..a5856b4e 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java +++ b/src/main/java/com/lovetropics/minigames/common/content/crafting_bee/ingredient/SimpleTagToItemDecomposer.java @@ -14,7 +14,7 @@ public record SimpleTagToItemDecomposer() implements IngredientDecomposer { public @Nullable List decompose(Ingredient ingredient) { // This is a "hack". Neo will sometimes replace a vanilla recipe with a difference ingredient (#chests - #chests/trapped) // we just resolve it and return the first item - if (ingredient.getCustomIngredient() instanceof DifferenceIngredient) { + if (ingredient.getCustomIngredient() != null) { return List.of(Ingredient.of(ingredient.getItems()[0])); } else if (ingredient.getValues().length == 1 && ingredient.getValues()[0] instanceof Ingredient.TagValue) { var items = ingredient.getValues()[0].getItems(); diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java index f644f732..910ba084 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/behavior/instances/SetGameClientStateBehavior.java @@ -3,6 +3,7 @@ import com.lovetropics.minigames.common.core.game.IGamePhase; import com.lovetropics.minigames.common.core.game.behavior.IGameBehavior; import com.lovetropics.minigames.common.core.game.behavior.event.EventRegistrar; +import com.lovetropics.minigames.common.core.game.behavior.event.GamePhaseEvents; import com.lovetropics.minigames.common.core.game.client_state.GameClientState; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -15,5 +16,6 @@ public record SetGameClientStateBehavior(GameClientState state) implements IGame @Override public void register(IGamePhase game, EventRegistrar events) { GameClientState.applyGlobally(state, events); + events.listen(GamePhaseEvents.STOP, reason -> GameClientState.removeFromPlayers(state.getType(), game.allPlayers())); } }