diff --git a/src/generated/resources/assets/ltminigames/lang/en_ud.json b/src/generated/resources/assets/ltminigames/lang/en_ud.json index 31e1a2f0..07abfe1d 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_ud.json +++ b/src/generated/resources/assets/ltminigames/lang/en_ud.json @@ -435,6 +435,9 @@ "ltminigames.minigame.river_race.microgames_results": ":sʇןnsǝɹ ǝɥʇ ǝɹɐ ǝɹǝH ¡pǝʇǝןdɯoɔ ǝʌɐɥ sǝɯɐboɹɔıW", "ltminigames.minigame.river_race.player_has_collectable": "¡ǝןqɐʇɔǝןןoɔ sıɥʇ sɐɥ ʎpɐǝɹןɐ %s", "ltminigames.minigame.river_race.shop": "doɥS", + "ltminigames.minigame.river_race.sidebar.team_progress": "%s%% - %s ", + "ltminigames.minigame.river_race.sidebar.victory_points": "ǝuoZ ɹǝd sʇuıoԀ ʎɹoʇɔıΛ", + "ltminigames.minigame.river_race.sidebar.zone_header": ":%s", "ltminigames.minigame.river_race.trivia.collectable_given": "¡ʇuǝɯnuoɯ ǝɥʇ oʇuı ǝɔɐןd oʇ ǝןqɐʇɔǝןןoɔ ɐ uǝʌıb uǝǝq ǝʌ,noʎ", "ltminigames.minigame.river_race.trivia.collectable_placed.subtitle": "ǝuoz %ǝɯɐu% pǝʇǝןdɯoƆ", "ltminigames.minigame.river_race.trivia.collectable_placed.title": "¡%ɯɐǝʇ% o⅁", diff --git a/src/generated/resources/assets/ltminigames/lang/en_us.json b/src/generated/resources/assets/ltminigames/lang/en_us.json index 0e391d3d..eb3c40b7 100644 --- a/src/generated/resources/assets/ltminigames/lang/en_us.json +++ b/src/generated/resources/assets/ltminigames/lang/en_us.json @@ -435,6 +435,9 @@ "ltminigames.minigame.river_race.microgames_results": "Microgames have completed! Here are the results:", "ltminigames.minigame.river_race.player_has_collectable": "%s already has this collectable!", "ltminigames.minigame.river_race.shop": "Shop", + "ltminigames.minigame.river_race.sidebar.team_progress": " %s - %s%%", + "ltminigames.minigame.river_race.sidebar.victory_points": "Victory Points per Zone", + "ltminigames.minigame.river_race.sidebar.zone_header": "%s:", "ltminigames.minigame.river_race.trivia.collectable_given": "You've been given a collectable to place into the monument!", "ltminigames.minigame.river_race.trivia.collectable_placed.subtitle": "Completed %name% zone", "ltminigames.minigame.river_race.trivia.collectable_placed.title": "Go %team%!", diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceState.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceState.java index 7fc53ebe..dcb24e3a 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceState.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/RiverRaceState.java @@ -1,17 +1,26 @@ package com.lovetropics.minigames.common.content.river_race; import com.lovetropics.lib.BlockBox; +import com.lovetropics.minigames.common.content.river_race.block.HasTrivia; +import com.lovetropics.minigames.common.content.river_race.block.TriviaType; +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 it.unimi.dsi.fastutil.longs.LongIterator; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.network.chat.Component; import net.minecraft.world.item.DyeColor; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class RiverRaceState implements IGameState { public static final GameStateKey.Defaulted KEY = GameStateKey.create("River Race", RiverRaceState::new); @@ -23,8 +32,27 @@ public void setForwardDirection(Direction forwardDirection) { this.forwardDirection = forwardDirection; } - public void addZone(String id, BlockBox box, Component displayName, DyeColor color) { - zones.add(new Zone(id, box, displayName, color)); + private Map findAllTriviaBlocks(IGamePhase game, BlockBox box) { + Map triviaBlocks = new HashMap<>(); + + LongIterator chunkIterator = box.asChunks().longIterator(); + while (chunkIterator.hasNext()) { + long chunkPos = chunkIterator.nextLong(); + LevelChunk chunk = game.level().getChunk(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos)); + for (Map.Entry entry : chunk.getBlockEntities().entrySet()) { + BlockPos pos = entry.getKey(); + if (box.contains(pos) && entry.getValue() instanceof HasTrivia hasTrivia) { + triviaBlocks.put(pos, hasTrivia.getTriviaType()); + } + } + } + + return triviaBlocks; + } + + public void addZone(IGamePhase game, String id, BlockBox box, Component displayName, DyeColor color) { + Map triviaBlocks = findAllTriviaBlocks(game, box); + zones.add(new Zone(id, box, displayName, color, triviaBlocks)); } public Zone getZoneById(String id) { @@ -46,6 +74,14 @@ public Zone getZoneByPos(BlockPos pos) { return null; } + public Zone getFirstZone() { + return zones.getFirst(); + } + + public List getZones() { + return zones; + } + // Mirrored for the opposite team side so that equivalent trivia blocks can reuse the same question public ZoneLocalPos getZoneLocalPosKey(Zone zone, BlockPos pos) { pos = pos.subtract(zone.box.min()); @@ -78,15 +114,17 @@ public static final class Zone { private final BlockBox box; private final Component displayName; private final DyeColor color; + private final Map triviaBlocks; @Nullable private ItemStack collectable; - public Zone(String id, BlockBox box, Component displayName, DyeColor color) { + public Zone(String id, BlockBox box, Component displayName, DyeColor color, Map triviaBlocks) { this.id = id; this.box = box; this.displayName = displayName; this.color = color; + this.triviaBlocks = triviaBlocks; } public String id() { @@ -105,6 +143,10 @@ public DyeColor color() { return color; } + public Map triviaBlocks() { + return triviaBlocks; + } + @Nullable public ItemStack collectable() { return collectable; 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 6898c726..6e2e8a3a 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 @@ -33,6 +33,10 @@ public final class RiverRaceTexts { public static final Component MICROGAME_RESULTS = KEYS.add("microgames_results", "Microgames have completed! Here are the results:") .withStyle(ChatFormatting.GOLD); + public static final Component SIDEBAR_VICTORY_POINTS = KEYS.add("sidebar.victory_points", "Victory Points per Zone").withStyle(ChatFormatting.GREEN); + public static final TranslationCollector.Fun1 SIDEBAR_ZONE_HEADER = KEYS.add1("sidebar.zone_header", "%s:"); + public static final TranslationCollector.Fun2 SIDEBAR_TEAM_PROGRESS = KEYS.add2("sidebar.team_progress", " %s - %s%%"); + public static final TranslationCollector.Fun1 COLLECTABLE_NAME = KEYS.add1("collectable_name", "Collectable - %s"); public static void collectTranslations(BiConsumer consumer) { diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/CollectablesBehaviour.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/CollectablesBehaviour.java index 9bb5d3e6..0e98c047 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/CollectablesBehaviour.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/CollectablesBehaviour.java @@ -134,7 +134,7 @@ private InteractionResult onCollectablePlaced(IGamePhase game, ServerPlayer play if (firstTeamToCollect.putIfAbsent(collectableZone.id(), team.key()) == null) { GameActionContext context = GameActionContext.builder() .set(GameActionParameter.TEAM, team) - .set(GameActionParameter.NAME, collectableZone.displayName().copy().withColor(collectableZone.color().getTextureDiffuseColor())) + .set(GameActionParameter.NAME, collectableZone.displayName()) .build(); CollectableConfig config = collectablesByZone.get(collectableZone.id()); if (config != null) { @@ -148,7 +148,7 @@ private InteractionResult onCollectablePlaced(IGamePhase game, ServerPlayer play private ItemStack createItem(RiverRaceState.Zone zone, CollectableConfig collectable) { ItemStack item = collectable.baseItem.copy(); - item.set(DataComponents.ITEM_NAME, RiverRaceTexts.COLLECTABLE_NAME.apply(zone.displayName()).withColor(zone.color().getTextureDiffuseColor())); + item.set(DataComponents.ITEM_NAME, RiverRaceTexts.COLLECTABLE_NAME.apply(zone.displayName())); item.set(RiverRace.COLLECTABLE_MARKER.get(), Unit.INSTANCE); item.applyComponents(itemPatch); return item; diff --git a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceZoneBehavior.java b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceZoneBehavior.java index 073dbd63..64cc4595 100644 --- a/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceZoneBehavior.java +++ b/src/main/java/com/lovetropics/minigames/common/content/river_race/behaviour/RiverRaceZoneBehavior.java @@ -30,6 +30,6 @@ public record RiverRaceZoneBehavior( public void register(IGamePhase game, EventRegistrar events) throws GameException { RiverRaceState riverRace = game.state().get(RiverRaceState.KEY); BlockBox box = game.mapRegions().getOrThrow(regionKey); - riverRace.addZone(id, box, displayName, color); + riverRace.addZone(game, id, box, displayName, color); } } 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 84743320..1a443e68 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 @@ -2,6 +2,7 @@ import com.lovetropics.lib.codec.MoreCodecs; import com.lovetropics.minigames.SoundRegistry; +import com.lovetropics.minigames.common.content.river_race.RiverRaceState; import com.lovetropics.minigames.common.content.river_race.RiverRaceTexts; import com.lovetropics.minigames.common.content.river_race.block.TriviaType; import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; @@ -22,20 +23,28 @@ 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.lovetropics.minigames.common.core.game.util.GameSidebar; +import com.lovetropics.minigames.common.core.game.util.GlobalGameWidgets; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.ChatFormatting; +import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundSource; import net.minecraft.util.ExtraCodecs; import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class VictoryPointsBehavior implements IGameBehavior { @@ -48,7 +57,14 @@ public class VictoryPointsBehavior implements IGameBehavior { ExtraCodecs.nonEmptyList(MoreCodecs.listOrUnit(Codec.INT)).optionalFieldOf("points_per_game_won", List.of(3, 2, 1)).forGetter(c -> c.pointsPerGameWon) ).apply(i, VictoryPointsBehavior::new)); + private static final int SIDEBAR_INTERVAL = SharedConstants.TICKS_PER_SECOND / 2; + private IGamePhase game; + private TeamState teams; + private final Object2IntMap availablePointsPerZone = new Object2IntOpenHashMap<>(); + private final Map> acquiredPointsPerZone = new HashMap<>(); + + private RiverRaceState.Zone currentZone; private final int triviaChestPoints; private final int triviaGatePoints; @@ -72,7 +88,20 @@ public VictoryPointsBehavior(int triviaChestPoints, int triviaGatePoints, int tr @Override public void register(IGamePhase game, EventRegistrar events) throws GameException { this.game = game; - TeamState teams = game.instanceState().getOrThrow(TeamState.KEY); + teams = game.instanceState().getOrThrow(TeamState.KEY); + + RiverRaceState riverRace = game.state().get(RiverRaceState.KEY); + for (RiverRaceState.Zone zone : riverRace.getZones()) { + availablePointsPerZone.put(zone.id(), computeAvailablePoints(zone)); + } + + for (GameTeam team : teams) { + acquiredPointsPerZone.put(team.key(), new Object2IntOpenHashMap<>()); + } + + currentZone = riverRace.getFirstZone(); + + GameSidebar sidebar = GlobalGameWidgets.registerTo(game, events).openSidebar(game.definition().name().copy().withStyle(ChatFormatting.AQUA)); events.listen(RiverRaceEvents.QUESTION_COMPLETED, this::onQuestionAnswered); events.listen(RiverRaceEvents.COLLECTABLE_PLACED, this::onCollectablePlaced); @@ -91,6 +120,10 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio game.invoker(RiverRaceEvents.VICTORY_POINTS_CHANGED).onVictoryPointsChanged(teamKey, newPoints, oldPoints); } } + + if (game.ticks() % SIDEBAR_INTERVAL == 0) { + sidebar.set(renderSidebar(teams)); + } }); events.listen(SubGameEvents.CREATE, (subGame, subEvents) -> { @@ -106,6 +139,24 @@ public void register(IGamePhase game, EventRegistrar events) throws GameExceptio microgameSegment = null; } }); + + events.listen(RiverRaceEvents.UNLOCK_ZONE, id -> + currentZone = riverRace.getZoneById(id) + ); + } + + private int computeAvailablePoints(RiverRaceState.Zone zone) { + int availablePoints = 0; + for (TriviaType type : zone.triviaBlocks().values()) { + availablePoints += getPointsForTriviaType(type); + } + if (zone.collectable() != null) { + availablePoints += collectablePlacedPoints * teams.size(); + } + if (availablePoints % teams.size() != 0) { + throw new GameException(Component.literal("Uneven point balance between teams")); + } + return availablePoints / teams.size(); } private void onMicrogameWinTriggered(GameWinner winner, MicrogameSegmentState segmentState) { @@ -126,7 +177,7 @@ private void onMicrogamesCompleted(MicrogameSegmentState segmentState) { GameStatistics segmentStatistics = new GameStatistics(); for (Object2IntMap.Entry entry : segmentState.pointsByTeam.object2IntEntrySet()) { - addPoints(entry.getKey(), entry.getIntValue()); + addPoints(entry.getKey(), entry.getIntValue(), false); segmentStatistics.forTeam(entry.getKey()).set(StatisticKey.VICTORY_POINTS, entry.getIntValue()); } @@ -141,30 +192,59 @@ private GameTeamKey getTeamFor(PlayerKey player) { return teams != null ? teams.getTeamForPlayer(player) : null; } - private void addPoints(final PlayerKey playerKey, final int points) { + private void addPoints(final PlayerKey playerKey, final int points, boolean inZone) { TeamState teams = game.instanceState().getOrNull(TeamState.KEY); GameTeamKey team = teams != null ? teams.getTeamForPlayer(playerKey) : null; if (team != null) { - game.statistics().forTeam(team).incrementInt(StatisticKey.VICTORY_POINTS, points); + addPoints(team, points, inZone); } } - private void addPoints(final GameTeamKey team, final int points) { + private void addPoints(final GameTeamKey team, final int points, final boolean inZone) { game.statistics().forTeam(team).incrementInt(StatisticKey.VICTORY_POINTS, points); + if (inZone) { + acquiredPointsPerZone.get(team).addTo(currentZone.id(), points); + } } private void onQuestionAnswered(ServerPlayer player, TriviaType triviaType, BlockPos triviaPos) { - int points = switch (triviaType) { + addPoints(PlayerKey.from(player), getPointsForTriviaType(triviaType), true); + } + + private int getPointsForTriviaType(TriviaType triviaType) { + return switch (triviaType) { case COLLECTABLE -> collectableCollectedPoints; case VICTORY -> triviaChallengePoints; case REWARD -> triviaChestPoints; case GATE -> triviaGatePoints; }; - addPoints(PlayerKey.from(player), points); } private void onCollectablePlaced(ServerPlayer player, GameTeam team, BlockPos pos) { - addPoints(PlayerKey.from(player), collectablePlacedPoints); + addPoints(PlayerKey.from(player), collectablePlacedPoints, true); + } + + private Component[] renderSidebar(TeamState teams) { + RiverRaceState riverRace = game.state().get(RiverRaceState.KEY); + + List sidebar = new ArrayList<>(10); + sidebar.add(RiverRaceTexts.SIDEBAR_VICTORY_POINTS); + + for (RiverRaceState.Zone zone : riverRace.getZones()) { + int pointsInZone = availablePointsPerZone.getInt(zone.id()); + if (pointsInZone == 0) { + continue; + } + sidebar.add(CommonComponents.EMPTY); + sidebar.add(RiverRaceTexts.SIDEBAR_ZONE_HEADER.apply(zone.displayName())); + for (GameTeam team : teams) { + int acquiredPoints = acquiredPointsPerZone.get(team.key()).getInt(zone.id()); + int percent = acquiredPoints * 100 / pointsInZone; + sidebar.add(RiverRaceTexts.SIDEBAR_TEAM_PROGRESS.apply(team.config().styledName(), percent)); + } + } + + return sidebar.toArray(new Component[0]); } private static final class MicrogameSegmentState { 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 dce47c60..54597d95 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 @@ -114,7 +114,7 @@ GameResult start(final boolean savePlayerDataToMemory) { } catch (GameException e) { return GameResult.error(e.getTextMessage()); } catch (Exception e) { - return GameResult.error(Component.literal(e.getMessage())); + return GameResult.error(Component.literal(e.getClass() + ": " + e.getMessage())); } final String mapName = map.name();