From 27c50e4c19a92b46883161cdb9c24f5af0dc7db0 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 8 Sep 2026 09:13:05 +0200 Subject: [PATCH 1/8] feat(tag): add rank tag and registry --- .../cygnus/common/rank/RankTag.java | 74 ++++++ .../cygnus/common/rank/RankTagRegistry.java | 215 ++++++++++++++++++ .../cygnus/common/rank/package-info.java | 4 + 3 files changed, 293 insertions(+) create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java new file mode 100644 index 00000000..44a64fe0 --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java @@ -0,0 +1,74 @@ +package net.onelitefeather.cygnus.common.rank; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; + +import java.util.Objects; + +/** + * Represents a graphical rank tag rendered with a custom font glyph. + * + * @param id the unique identifier of the rank tag + * @param glyph the character for the tag bitmap + * @param font the resource pack font key + * @param priority the priority used for resolution (higher priority takes precedence) + * @param asComponent the pre-built {@link Component} for rendering + * @author theEvilReaper + * @version 1.0.0 + * @since 2.8.0 + */ +public record RankTag( + String id, + String glyph, + Key font, + int priority, + Component asComponent +) implements Comparable { + + /** + * The default font key for player rank tags defined in the resource pack. + */ + public static final Key DEFAULT_FONT = Key.key("olf", "rank_tags"); + + public static final RankTag ADMINISTRATOR = new RankTag("administrator", "󰆐", DEFAULT_FONT, 100); + public static final RankTag ASSISTANT = new RankTag("assistent", "󲆛", DEFAULT_FONT, 90); + public static final RankTag MOD = new RankTag("mod", "󱆛", DEFAULT_FONT, 80); + public static final RankTag CONTENT = new RankTag("content", "󲆐", DEFAULT_FONT, 70); + public static final RankTag MEDIA = new RankTag("media", "󲆕", DEFAULT_FONT, 60); + public static final RankTag LITE = new RankTag("lite", "󲆑", DEFAULT_FONT, 50); + public static final RankTag PLAYER = new RankTag("player", "󱆖", DEFAULT_FONT, 0); + + /** + * Creates a new rank tag and builds the cached {@link Component}. + * + * @param id the unique identifier of the rank tag + * @param glyph the unicode character for the tag bitmap + * @param font the resource pack font key + * @param priority the priority used for resolution + */ + public RankTag(String id, String glyph, Key font, int priority) { + this( + Objects.requireNonNull(id, "id must not be null"), + Objects.requireNonNull(glyph, "glyph must not be null"), + Objects.requireNonNull(font, "font must not be null"), + priority, + Component.text(glyph).font(font) + ); + } + + /** + * Creates a new rank tag using {@link #DEFAULT_FONT}. + * + * @param id the unique identifier of the rank tag + * @param glyph the unicode character for the tag bitmap + * @param priority the priority used for resolution + */ + public RankTag(String id, String glyph, int priority) { + this(id, glyph, DEFAULT_FONT, priority); + } + + @Override + public int compareTo(RankTag other) { + return Integer.compare(other.priority, this.priority); + } +} diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java new file mode 100644 index 00000000..ee81d78a --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java @@ -0,0 +1,215 @@ +package net.onelitefeather.cygnus.common.rank; + +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import net.minestom.server.entity.Player; +import net.onelitefeather.cygnus.common.permission.LuckPermsSupport; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registry and resolver for mapping LuckPerms ranks/groups to {@link RankTag}s. + * + * @author theEvilReaper + * @version 1.0.0 + * @since 2.8.0 + */ +public final class RankTagRegistry { + + private static final RankTagRegistry STANDARD = new RankTagRegistry(); + + static { + STANDARD.registerDefaults(); + } + + private final Map tagsById = new ConcurrentHashMap<>(); + private final Map tagsByGroup = new ConcurrentHashMap<>(); + private volatile RankTag fallbackTag = RankTag.PLAYER; + + /** + * Returns the global default {@link RankTagRegistry} instance pre-configured with standard tags. + * + * @return the shared standard registry + */ + public static RankTagRegistry standard() { + return STANDARD; + } + + /** + * Registers the default OLF rank tags and standard group aliases. + */ + public void registerDefaults() { + register(RankTag.ADMINISTRATOR, "administrator", "admin", "owner"); + register(RankTag.ASSISTANT, "assistent", "assistant", "sr_mod", "srmod"); + register(RankTag.MOD, "mod", "moderator"); + register(RankTag.CONTENT, "content", "developer", "dev", "builder"); + register(RankTag.MEDIA, "media", "creator", "youtube", "twitch"); + register(RankTag.LITE, "lite", "vip", "premium"); + register(RankTag.PLAYER, "player", "default"); + } + + /** + * Registers a {@link RankTag} along with associated LuckPerms group aliases. + * + * @param tag the rank tag to register + * @param groupAliases group names that map to this tag (case-insensitive) + */ + public void register(RankTag tag, String... groupAliases) { + Objects.requireNonNull(tag, "tag must not be null"); + this.tagsById.put(tag.id().toLowerCase(Locale.ROOT), tag); + + for (String alias : groupAliases) { + if (!alias.isBlank()) { + this.tagsByGroup.put(alias.toLowerCase(Locale.ROOT), tag); + } + } + } + + /** + * Finds a registered tag by its unique identifier. + * + * @param id the tag id + * @return the optional rank tag + */ + public Optional findById(@Nullable String id) { + if (id == null) return Optional.empty(); + return Optional.ofNullable(this.tagsById.get(id.toLowerCase(Locale.ROOT))); + } + + /** + * Resolves a rank tag for a specific group name. + * + * @param groupName the name of the group + * @return the matching {@link RankTag}, or empty if unmapped + */ + public Optional resolveByGroup(@Nullable String groupName) { + if (groupName == null || groupName.isBlank()) { + return Optional.empty(); + } + return Optional.ofNullable(this.tagsByGroup.get(groupName.toLowerCase(Locale.ROOT))); + } + + /** + * Resolves the primary {@link RankTag} for a player with highest priority. + * + * @param player the player to resolve for + * @return the resolved {@link RankTag}, or {@link #getFallbackTag()} if unresolvable + */ + public RankTag resolvePrimary(Player player) { + return resolvePrimary(player.getUuid()); + } + + /** + * Resolves the primary {@link RankTag} for a player's UUID by checking LuckPerms groups. + * + * @param uuid the unique id of the player + * @return the resolved {@link RankTag} with highest priority, or {@link #getFallbackTag()} + */ + public RankTag resolvePrimary(UUID uuid) { + if (!LuckPermsSupport.isPresent()) { + return this.fallbackTag; + } + + try { + User user = LuckPermsProvider.get().getUserManager().getUser(uuid); + if (user == null) { + return this.fallbackTag; + } + + List matchedTags = resolveUserTags(user); + if (matchedTags.isEmpty()) { + return resolveByGroup(user.getPrimaryGroup()).orElse(this.fallbackTag); + } + + return matchedTags.getFirst(); + } catch (Exception _) { + return this.fallbackTag; + } + } + + /** + * Resolves all available {@link RankTag}s a player is eligible for, sorted by priority descending. + * Useful for allowing the player to select/rotate between multiple unlocked tags. + * + * @param uuid the unique id of the player + * @return a list of all matching rank tags, sorted by priority (highest first) + */ + public List resolveAvailable(UUID uuid) { + if (uuid == null || !LuckPermsSupport.isPresent()) { + return List.of(this.fallbackTag); + } + + try { + User user = LuckPermsProvider.get().getUserManager().getUser(uuid); + if (user == null) { + return List.of(this.fallbackTag); + } + + List matched = resolveUserTags(user); + return matched.isEmpty() ? List.of(this.fallbackTag) : matched; + } catch (Exception _) { + return List.of(this.fallbackTag); + } + } + + /** + * Internal helper to collect all distinct mapped {@link RankTag}s for a LuckPerms {@link User}. + */ + private List resolveUserTags(User user) { + Set matched = new HashSet<>(); + + // Check primary group + resolveByGroup(user.getPrimaryGroup()).ifPresent(matched::add); + + // Check all inherited groups + Collection nodes = user.getNodes(NodeType.INHERITANCE); + for (InheritanceNode node : nodes) { + resolveByGroup(node.getGroupName()).ifPresent(matched::add); + } + + return matched.stream() + .sorted(Comparator.comparingInt(RankTag::priority).reversed()) + .toList(); + } + + /** + * Returns the fallback tag when no matching group is found or LuckPerms is absent. + * + * @return the fallback rank tag + */ + public RankTag getFallbackTag() { + return this.fallbackTag; + } + + /** + * Sets the fallback tag used when no matching group is found. + * + * @param fallbackTag the new fallback tag + */ + public void setFallbackTag(RankTag fallbackTag) { + this.fallbackTag = Objects.requireNonNull(fallbackTag, "fallbackTag must not be null"); + } + + /** + * Returns an unmodifiable collection of all registered tags. + * + * @return collection of registered rank tags + */ + public Collection getAllTags() { + return Collections.unmodifiableCollection(this.tagsById.values()); + } +} diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java new file mode 100644 index 00000000..2003b838 --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.onelitefeather.cygnus.common.rank; + +import org.jetbrains.annotations.NotNullByDefault; From 3b614a4c0689a5c4511ccbf67de1b947a54de42c Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 8 Sep 2026 09:13:18 +0200 Subject: [PATCH 2/8] chore(tags): add rank tag variable --- common/src/main/java/net/onelitefeather/cygnus/common/Tags.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/Tags.java b/common/src/main/java/net/onelitefeather/cygnus/common/Tags.java index 45b33727..8364b854 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/Tags.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/Tags.java @@ -2,6 +2,7 @@ import net.kyori.adventure.key.Key; import net.minestom.server.tag.Tag; +import net.onelitefeather.cygnus.common.rank.RankTag; import java.util.UUID; @@ -18,6 +19,7 @@ public final class Tags { public static final Tag ITEM_TAG = Tag.Byte("itemTag"); public static final Tag TEAM_KEY = Tag.Transient("teamKey"); public static final Tag HIDDEN = Tag.Byte("hidden"); + public static final Tag ACTIVE_RANK_TAG = Tag.Transient("activeRankTag"); private Tags() { // Nothing do to here From cc68fd3621fbff1636b7ad5669d8f0c2014e10c4 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 8 Sep 2026 09:14:35 +0200 Subject: [PATCH 3/8] test(rank): add test cases --- .../common/rank/RankTagRegistryTest.java | 112 ++++++++++++++++++ .../cygnus/common/rank/RankTagTest.java | 33 ++++++ 2 files changed, 145 insertions(+) create mode 100644 common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java create mode 100644 common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java new file mode 100644 index 00000000..1fa56a4f --- /dev/null +++ b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java @@ -0,0 +1,112 @@ +package net.onelitefeather.cygnus.common.rank; + +import net.kyori.adventure.key.Key; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RankTagRegistryTest { + + private RankTagRegistry registry; + + @BeforeEach + void setUp() { + this.registry = new RankTagRegistry(); + this.registry.registerDefaults(); + } + + @Test + void testStandardDefaults() { + RankTagRegistry standard = RankTagRegistry.standard(); + assertNotNull(standard); + + assertTrue(standard.findById("administrator").isPresent()); + assertTrue(standard.findById("assistent").isPresent()); + assertTrue(standard.findById("mod").isPresent()); + assertTrue(standard.findById("content").isPresent()); + assertTrue(standard.findById("media").isPresent()); + assertTrue(standard.findById("lite").isPresent()); + assertTrue(standard.findById("player").isPresent()); + } + + @Test + void testResolveByGroupStandardAndAliases() { + assertEquals(Optional.of(RankTag.ADMINISTRATOR), registry.resolveByGroup("administrator")); + assertEquals(Optional.of(RankTag.ADMINISTRATOR), registry.resolveByGroup("admin")); + assertEquals(Optional.of(RankTag.ADMINISTRATOR), registry.resolveByGroup("ADMIN")); + assertEquals(Optional.of(RankTag.ADMINISTRATOR), registry.resolveByGroup("owner")); + + assertEquals(Optional.of(RankTag.ASSISTANT), registry.resolveByGroup("assistent")); + assertEquals(Optional.of(RankTag.ASSISTANT), registry.resolveByGroup("assistant")); + assertEquals(Optional.of(RankTag.ASSISTANT), registry.resolveByGroup("sr_mod")); + + assertEquals(Optional.of(RankTag.MOD), registry.resolveByGroup("mod")); + assertEquals(Optional.of(RankTag.MOD), registry.resolveByGroup("moderator")); + + assertEquals(Optional.of(RankTag.CONTENT), registry.resolveByGroup("content")); + assertEquals(Optional.of(RankTag.CONTENT), registry.resolveByGroup("developer")); + assertEquals(Optional.of(RankTag.CONTENT), registry.resolveByGroup("builder")); + + assertEquals(Optional.of(RankTag.MEDIA), registry.resolveByGroup("media")); + assertEquals(Optional.of(RankTag.MEDIA), registry.resolveByGroup("creator")); + assertEquals(Optional.of(RankTag.MEDIA), registry.resolveByGroup("twitch")); + + assertEquals(Optional.of(RankTag.LITE), registry.resolveByGroup("lite")); + assertEquals(Optional.of(RankTag.LITE), registry.resolveByGroup("vip")); + assertEquals(Optional.of(RankTag.LITE), registry.resolveByGroup("premium")); + + assertEquals(Optional.of(RankTag.PLAYER), registry.resolveByGroup("player")); + assertEquals(Optional.of(RankTag.PLAYER), registry.resolveByGroup("default")); + + assertEquals(Optional.empty(), registry.resolveByGroup("unknown_group")); + assertEquals(Optional.empty(), registry.resolveByGroup(null)); + assertEquals(Optional.empty(), registry.resolveByGroup(" ")); + } + + @Test + void testRegisterCustomTag() { + RankTag tester = new RankTag("tester", "󱆛", Key.key("olf", "rank_tags"), 30); + registry.register(tester, "tester", "qa_lead"); + + assertEquals(Optional.of(tester), registry.findById("tester")); + assertEquals(Optional.of(tester), registry.resolveByGroup("tester")); + assertEquals(Optional.of(tester), registry.resolveByGroup("qa_lead")); + } + + @Test + void testFallbackWhenLuckPermsAbsent() { + UUID testUuid = UUID.randomUUID(); + RankTag primary = registry.resolvePrimary(testUuid); + assertSame(RankTag.PLAYER, primary); + + List available = registry.resolveAvailable(testUuid); + assertEquals(1, available.size()); + assertSame(RankTag.PLAYER, available.getFirst()); + + assertSame(RankTag.PLAYER, registry.resolvePrimary((UUID) null)); + } + + @Test + void testCustomFallbackTag() { + RankTag customFallback = new RankTag("guest", "󱆖", -1); + registry.setFallbackTag(customFallback); + + assertSame(customFallback, registry.getFallbackTag()); + assertSame(customFallback, registry.resolvePrimary((UUID) null)); + } + + @Test + void testInvalidRegistrations() { + assertThrows(NullPointerException.class, () -> registry.register(null, "foo")); + assertThrows(NullPointerException.class, () -> registry.setFallbackTag(null)); + } +} diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java new file mode 100644 index 00000000..ed549b55 --- /dev/null +++ b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java @@ -0,0 +1,33 @@ +package net.onelitefeather.cygnus.common.rank; + +import net.kyori.adventure.text.Component; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RankTagTest { + + @Test + void testRankTagProperties() { + RankTag custom = new RankTag("custom", "󰆐", 42); + + assertEquals("custom", custom.id()); + assertEquals("󰆐", custom.glyph()); + assertEquals(RankTag.DEFAULT_FONT, custom.font()); + assertEquals(42, custom.priority()); + + Component comp = custom.asComponent(); + assertNotNull(comp); + assertEquals("󰆐", ((net.kyori.adventure.text.TextComponent) comp).content()); + assertEquals(RankTag.DEFAULT_FONT, comp.style().font()); + } + + @Test + void testRankTagComparison() { + assertTrue(RankTag.ADMINISTRATOR.compareTo(RankTag.MOD) < 0); + assertTrue(RankTag.MOD.compareTo(RankTag.ADMINISTRATOR) > 0); + assertEquals(0, RankTag.PLAYER.compareTo(new RankTag("other", "󱆖", 0))); + } +} From 9ee85487b315be553f2296fc2a68a384c75ad66e Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 8 Sep 2026 09:40:39 +0200 Subject: [PATCH 4/8] chore(game): integrate tag usage --- .../cygnus/listener/PlayerChatListener.java | 83 ++++++++++++++++--- .../cygnus/listener/PlayerSpawnListener.java | 11 ++- .../cygnus/spectator/SpectatorService.java | 8 +- .../cygnus/team/TeamHelper.java | 11 ++- 4 files changed, 96 insertions(+), 17 deletions(-) diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerChatListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerChatListener.java index e32dedbe..41a67f9a 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerChatListener.java +++ b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerChatListener.java @@ -2,11 +2,17 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; +import net.onelitefeather.cygnus.common.Tags; +import net.onelitefeather.cygnus.common.rank.RankTag; +import net.onelitefeather.cygnus.phase.GamePhase; import net.onelitefeather.cygnus.team.TeamHelper; +import net.theevilreaper.xerus.api.phase.Phase; import net.theevilreaper.xerus.api.team.Team; import java.util.function.Consumer; +import java.util.function.Supplier; /** * Formats every chat message and enforces the spectator chat isolation. @@ -25,7 +31,8 @@ * there and no additional phase guard is needed. * * @author TheMeinerLP - * @version 2.1.0 + * @author theEvilReaper + * @version 2.2.0 * @since 1.0.0 **/ public final class PlayerChatListener implements Consumer { @@ -33,20 +40,36 @@ public final class PlayerChatListener implements Consumer { private static final Component MESSAGE_PREFIX = Component.text("≫", NamedTextColor.YELLOW); private final Team spectatorTeam; + private final Supplier phaseSupplier; /** * Creates a new instance of the {@link PlayerChatListener}. * * @param spectatorTeam the team which receives the messages written by a spectator + * @param phaseSupplier supplier providing the currently active phase */ - public PlayerChatListener(Team spectatorTeam) { + public PlayerChatListener(Team spectatorTeam, Supplier phaseSupplier) { this.spectatorTeam = spectatorTeam; + this.phaseSupplier = phaseSupplier; + } + + /** + * Creates a new instance of the {@link PlayerChatListener} without phase supplier. + * + * @param spectatorTeam the team which receives the messages written by a spectator + */ + public PlayerChatListener(Team spectatorTeam) { + this(spectatorTeam, () -> null); } @Override public void accept(PlayerChatEvent event) { - //TODO: Improve chat during each phase - event.setFormattedMessage(this.setLobbyLayout(event)); + Phase phase = this.phaseSupplier.get(); + if (phase instanceof GamePhase) { + event.setFormattedMessage(this.setGameLayout(event)); + } else { + event.setFormattedMessage(this.setLobbyLayout(event)); + } if (!TeamHelper.isSpectatorTeam(event.getPlayer())) return; @@ -59,22 +82,58 @@ public void accept(PlayerChatEvent event) { } /** - * Builds the chat line for the given event. - *

- * The line is assembled below an empty root instead of below the display name: a child inherits every - * style its parent does not override, and the spectator display name is struck through, which would - * otherwise strike through the separator, the prefix and the message text as well. + * Builds the chat line during the lobby and restart phases. * * @param event the chat event to format * @return the formatted chat line */ private Component setLobbyLayout(PlayerChatEvent event) { + Player player = event.getPlayer(); + Component displayName = player.getDisplayName() != null + ? player.getDisplayName() + : Component.text(player.getUsername()); + + return Component.empty() + .append(displayName) + .append(Component.space()) + .append(MESSAGE_PREFIX) + .append(Component.space()) + .append(Component.text(event.getRawMessage(), NamedTextColor.GRAY)); + } + + /** + * Builds the chat line during active gameplay. + * + * @param event the chat event to format + * @return the formatted chat line + */ + private Component setGameLayout(PlayerChatEvent event) { + Player player = event.getPlayer(); + + if (TeamHelper.isSpectatorTeam(player)) { + RankTag tag = player.getTag(Tags.ACTIVE_RANK_TAG); + Component tagComponent = tag != null ? tag.asComponent().append(Component.space()) : Component.empty(); + + return Component.empty() + //TODO: Replace the [SPEC prefix in a later spec + .append(Component.text("[SPEC] ", NamedTextColor.DARK_GRAY)) + .append(tagComponent) + .append(Component.text(player.getUsername(), NamedTextColor.GRAY)) + .append(Component.space()) + .append(MESSAGE_PREFIX) + .append(Component.space()) + .append(Component.text(event.getRawMessage(), NamedTextColor.DARK_GRAY)); + } + + Component displayName = player.getDisplayName() != null + ? player.getDisplayName() + : Component.text(player.getUsername(), NamedTextColor.GREEN); + return Component.empty() - .append(event.getPlayer().getDisplayName()) + .append(displayName) .append(Component.space()) .append(MESSAGE_PREFIX) .append(Component.space()) - .append(Component.text(event.getRawMessage(), NamedTextColor.GRAY) - ); + .append(Component.text(event.getRawMessage(), NamedTextColor.GRAY)); } } diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java index a7a34443..e60232a4 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java +++ b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java @@ -5,9 +5,10 @@ import net.theevilreaper.xerus.api.phase.Phase; import net.kyori.adventure.text.Component; import net.minestom.server.entity.Player; -import net.minestom.server.event.player.PlayerSpawnEvent; import net.onelitefeather.cygnus.common.Messages; import net.onelitefeather.cygnus.common.Tags; +import net.onelitefeather.cygnus.common.rank.RankTag; +import net.onelitefeather.cygnus.common.rank.RankTagRegistry; import net.onelitefeather.cygnus.phase.LobbyPhase; import java.util.function.Consumer; @@ -33,7 +34,13 @@ public void accept(PlayerSpawnEvent event) { if (!event.isFirstSpawn()) return; Player player = event.getPlayer(); - player.setDisplayName(Component.text(player.getUsername())); + RankTag rankTag = player.getTag(Tags.ACTIVE_RANK_TAG); + if (rankTag == null) { + rankTag = RankTagRegistry.standard().resolvePrimary(player); + player.setTag(Tags.ACTIVE_RANK_TAG, rankTag); + } + + player.setDisplayName(rankTag.asComponent().append(Component.space()).append(Component.text(player.getUsername()))); if (phaseSupplier.get() instanceof LobbyPhase lobbyPhase) { Broadcaster.broadcast(Messages.getJoinMessage(player)); diff --git a/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java b/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java index 397659e0..a30c164a 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java +++ b/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java @@ -10,6 +10,7 @@ import net.minestom.server.event.player.PlayerUseItemEvent; import net.onelitefeather.cygnus.common.Tags; import net.onelitefeather.cygnus.common.config.GameConfig; +import net.onelitefeather.cygnus.common.rank.RankTag; import net.onelitefeather.cygnus.player.CygnusPlayer; import net.onelitefeather.cygnus.player.event.SpectatorAddEvent; import net.onelitefeather.cygnus.player.listener.SpectatorAddListener; @@ -90,7 +91,12 @@ public void join(Player player) { * @param player the player who just became a spectator */ private static void markAsSpectator(Player player) { - player.setDisplayName(Component.text(player.getUsername(), NamedTextColor.GRAY, TextDecoration.STRIKETHROUGH)); + RankTag tag = player.getTag(Tags.ACTIVE_RANK_TAG); + Component nameComponent = Component.text(player.getUsername(), NamedTextColor.GRAY, TextDecoration.STRIKETHROUGH); + Component displayName = tag != null + ? tag.asComponent().append(Component.space()).append(nameComponent) + : nameComponent; + player.setDisplayName(displayName); } /** diff --git a/game/src/main/java/net/onelitefeather/cygnus/team/TeamHelper.java b/game/src/main/java/net/onelitefeather/cygnus/team/TeamHelper.java index ade5f9fe..e5231722 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/team/TeamHelper.java +++ b/game/src/main/java/net/onelitefeather/cygnus/team/TeamHelper.java @@ -17,6 +17,7 @@ import net.onelitefeather.cygnus.common.Tags; import net.onelitefeather.cygnus.common.config.GameConfig; import net.onelitefeather.cygnus.common.map.GameMap; +import net.onelitefeather.cygnus.common.rank.RankTag; import java.util.HashSet; import java.util.Set; @@ -162,13 +163,19 @@ public static void updateTabList(TeamService teamService) { } slenderTeam.getPlayers().forEach(player -> { - Component slenderDisplayName = Component.text("⛧ ", NamedTextColor.RED) + RankTag tag = player.getTag(Tags.ACTIVE_RANK_TAG); + Component tagPart = tag != null ? tag.asComponent().append(Component.space()) : Component.empty(); + Component slenderDisplayName = tagPart + .append(Component.text("⛧ ", NamedTextColor.RED)) .append(Component.text(player.getUsername(), NamedTextColor.GRAY)); player.setDisplayName(slenderDisplayName); }); survivorTeam.getPlayers().forEach(player -> { - Component survivorDisplayName = Component.text(player.getUsername(), NamedTextColor.GREEN); + RankTag tag = player.getTag(Tags.ACTIVE_RANK_TAG); + Component survivorDisplayName = tag != null + ? tag.asComponent().append(Component.space()).append(Component.text(player.getUsername(), NamedTextColor.GREEN)) + : Component.text(player.getUsername(), NamedTextColor.GREEN); player.setDisplayName(survivorDisplayName); }); } From 03d57c913c2edc02ce7b45733224ea938cb0ebe0 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 8 Sep 2026 09:46:39 +0200 Subject: [PATCH 5/8] chore(game): add missing import --- .../net/onelitefeather/cygnus/listener/PlayerSpawnListener.java | 1 + 1 file changed, 1 insertion(+) diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java index e60232a4..7cdd3327 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java +++ b/game/src/main/java/net/onelitefeather/cygnus/listener/PlayerSpawnListener.java @@ -1,5 +1,6 @@ package net.onelitefeather.cygnus.listener; +import net.minestom.server.event.player.PlayerSpawnEvent; import net.theevilreaper.aves.util.Broadcaster; import net.theevilreaper.aves.util.functional.PlayerConsumer; import net.theevilreaper.xerus.api.phase.Phase; From 4435d93043d80ee1dfc5637b19601aa3786d11d6 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 8 Sep 2026 13:01:36 +0200 Subject: [PATCH 6/8] chore(tag): add missing component reset --- .../cygnus/common/rank/RankTag.java | 30 ++++++++++++++++++- .../cygnus/common/rank/RankTagRegistry.java | 2 +- .../common/rank/RankTagRegistryTest.java | 22 ++------------ .../cygnus/common/rank/RankTagTest.java | 27 +++++++++++++++++ 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java index 44a64fe0..01c0e73e 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTag.java @@ -30,8 +30,13 @@ public record RankTag( */ public static final Key DEFAULT_FONT = Key.key("olf", "rank_tags"); + /** + * The Minecraft default font key used to reset typography after custom font glyphs. + */ + public static final Key DEFAULT_MINECRAFT_FONT = Key.key("minecraft", "default"); + public static final RankTag ADMINISTRATOR = new RankTag("administrator", "󰆐", DEFAULT_FONT, 100); - public static final RankTag ASSISTANT = new RankTag("assistent", "󲆛", DEFAULT_FONT, 90); + public static final RankTag ASSISTENT = new RankTag("assistent", "󲆛", DEFAULT_FONT, 90); public static final RankTag MOD = new RankTag("mod", "󱆛", DEFAULT_FONT, 80); public static final RankTag CONTENT = new RankTag("content", "󲆐", DEFAULT_FONT, 70); public static final RankTag MEDIA = new RankTag("media", "󲆕", DEFAULT_FONT, 60); @@ -71,4 +76,27 @@ public RankTag(String id, String glyph, int priority) { public int compareTo(RankTag other) { return Integer.compare(other.priority, this.priority); } + + /** + * Formats a component by prepending this rank tag and explicitly resetting the font for subsequent text. + * + * @param trailing the trailing component to append after the tag + * @return the combined component with the trailing font explicitly reset to default + */ + public Component format(Component trailing) { + return Component.empty() + .append(this.asComponent) + .append(Component.space().font(DEFAULT_MINECRAFT_FONT)) + .append(trailing.font(DEFAULT_MINECRAFT_FONT)); + } + + /** + * Formats a text string by prepending this rank tag and explicitly resetting the font for subsequent text. + * + * @param text the text to append after the tag + * @return the combined component with default font + */ + public Component format(String text) { + return format(Component.text(text)); + } } diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java index ee81d78a..17ee7268 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/rank/RankTagRegistry.java @@ -54,7 +54,7 @@ public static RankTagRegistry standard() { */ public void registerDefaults() { register(RankTag.ADMINISTRATOR, "administrator", "admin", "owner"); - register(RankTag.ASSISTANT, "assistent", "assistant", "sr_mod", "srmod"); + register(RankTag.ASSISTENT, "assistent", "assistant", "sr_mod", "srmod"); register(RankTag.MOD, "mod", "moderator"); register(RankTag.CONTENT, "content", "developer", "dev", "builder"); register(RankTag.MEDIA, "media", "creator", "youtube", "twitch"); diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java index 1fa56a4f..6beb68c5 100644 --- a/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java +++ b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagRegistryTest.java @@ -9,10 +9,8 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; class RankTagRegistryTest { @@ -24,20 +22,6 @@ void setUp() { this.registry.registerDefaults(); } - @Test - void testStandardDefaults() { - RankTagRegistry standard = RankTagRegistry.standard(); - assertNotNull(standard); - - assertTrue(standard.findById("administrator").isPresent()); - assertTrue(standard.findById("assistent").isPresent()); - assertTrue(standard.findById("mod").isPresent()); - assertTrue(standard.findById("content").isPresent()); - assertTrue(standard.findById("media").isPresent()); - assertTrue(standard.findById("lite").isPresent()); - assertTrue(standard.findById("player").isPresent()); - } - @Test void testResolveByGroupStandardAndAliases() { assertEquals(Optional.of(RankTag.ADMINISTRATOR), registry.resolveByGroup("administrator")); @@ -45,9 +29,9 @@ void testResolveByGroupStandardAndAliases() { assertEquals(Optional.of(RankTag.ADMINISTRATOR), registry.resolveByGroup("ADMIN")); assertEquals(Optional.of(RankTag.ADMINISTRATOR), registry.resolveByGroup("owner")); - assertEquals(Optional.of(RankTag.ASSISTANT), registry.resolveByGroup("assistent")); - assertEquals(Optional.of(RankTag.ASSISTANT), registry.resolveByGroup("assistant")); - assertEquals(Optional.of(RankTag.ASSISTANT), registry.resolveByGroup("sr_mod")); + assertEquals(Optional.of(RankTag.ASSISTENT), registry.resolveByGroup("assistent")); + assertEquals(Optional.of(RankTag.ASSISTENT), registry.resolveByGroup("assistant")); + assertEquals(Optional.of(RankTag.ASSISTENT), registry.resolveByGroup("sr_mod")); assertEquals(Optional.of(RankTag.MOD), registry.resolveByGroup("mod")); assertEquals(Optional.of(RankTag.MOD), registry.resolveByGroup("moderator")); diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java index ed549b55..647fd523 100644 --- a/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java +++ b/common/src/test/java/net/onelitefeather/cygnus/common/rank/RankTagTest.java @@ -3,6 +3,8 @@ import net.kyori.adventure.text.Component; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -30,4 +32,29 @@ void testRankTagComparison() { assertTrue(RankTag.MOD.compareTo(RankTag.ADMINISTRATOR) > 0); assertEquals(0, RankTag.PLAYER.compareTo(new RankTag("other", "󱆖", 0))); } + + @Test + void testRankTagFormat() { + Component formatted = RankTag.ADMINISTRATOR.format("Steve"); + assertNotNull(formatted); + List children = formatted.children(); + assertEquals(3, children.size()); + assertEquals(RankTag.ADMINISTRATOR.asComponent(), children.get(0)); + assertEquals(RankTag.DEFAULT_MINECRAFT_FONT, children.get(1).style().font()); + assertEquals(RankTag.DEFAULT_MINECRAFT_FONT, children.get(2).style().font()); + } + + @Test + void testStandardDefaults() { + RankTagRegistry standard = RankTagRegistry.standard(); + assertNotNull(standard); + + assertTrue(standard.findById("administrator").isPresent()); + assertTrue(standard.findById("assistent").isPresent()); + assertTrue(standard.findById("mod").isPresent()); + assertTrue(standard.findById("content").isPresent()); + assertTrue(standard.findById("media").isPresent()); + assertTrue(standard.findById("lite").isPresent()); + assertTrue(standard.findById("player").isPresent()); + } } From d0ee2fb6250904d496a656c897852ce6a3e44e6d Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Tue, 8 Sep 2026 13:02:10 +0200 Subject: [PATCH 7/8] test(chat): add new test case --- .../listener/PlayerChatListenerTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerChatListenerTest.java b/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerChatListenerTest.java index b7f3ee33..89d47da9 100644 --- a/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerChatListenerTest.java +++ b/game/src/test/java/net/onelitefeather/cygnus/listener/PlayerChatListenerTest.java @@ -16,6 +16,7 @@ import net.onelitefeather.cygnus.CygnusPlayerTestBase; import net.onelitefeather.cygnus.common.Tags; import net.onelitefeather.cygnus.common.config.GameConfig; +import net.onelitefeather.cygnus.common.rank.RankTag; import net.theevilreaper.xerus.api.team.Team; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -23,6 +24,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -97,6 +99,25 @@ void testSpectatorMessageIsNotStruckThroughBehindTheName(@NotNull Env env) { env.destroyInstance(instance, true); } + @Test + void testLobbyChatLayoutWithRankTag(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + player.setTag(Tags.ACTIVE_RANK_TAG, RankTag.ADMINISTRATOR); + player.setDisplayName(RankTag.ADMINISTRATOR.format(player.getUsername())); + + Team spectatorTeam = Team.of(GameConfig.SPECTATOR_KEY, 5); + PlayerChatEvent event = new PlayerChatEvent(player, List.of(player), "hello lobby"); + new PlayerChatListener(spectatorTeam).accept(event); + + Component formatted = event.getFormattedMessage(); + assertNotNull(formatted); + List children = formatted.children(); + assertTrue(children.size() >= 4); + + env.destroyInstance(instance, true); + } + @Test void testSurvivorMessageReachesEveryone(@NotNull Env env) { assertChatReachesEveryone(env, GameConfig.SURVIVOR_KEY); From e1e6296aa4937be6d8dc93084de7aeab3e1e35da Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Sat, 12 Sep 2026 10:26:24 +0200 Subject: [PATCH 8/8] chore(tags): add more resource pack mapping --- .../cygnus/common/icon/GameIcon.java | 98 +++++++++++++++++++ .../cygnus/common/tag/GameTag.java | 82 ++++++++++++++++ .../cygnus/common/icon/GameIconTest.java | 43 ++++++++ .../cygnus/common/tag/GameTagTest.java | 34 +++++++ 4 files changed, 257 insertions(+) create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/icon/GameIcon.java create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/tag/GameTag.java create mode 100644 common/src/test/java/net/onelitefeather/cygnus/common/icon/GameIconTest.java create mode 100644 common/src/test/java/net/onelitefeather/cygnus/common/tag/GameTagTest.java diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/icon/GameIcon.java b/common/src/main/java/net/onelitefeather/cygnus/common/icon/GameIcon.java new file mode 100644 index 00000000..84ee8163 --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/icon/GameIcon.java @@ -0,0 +1,98 @@ +package net.onelitefeather.cygnus.common.icon; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +//import net.onelitefeather.cygnus.common.rank.RankTag; + +/** + * Represents custom game, role, and UI icons (Ghost, Flashlight, Pentagram, Clock, Page, Map, Builder) + * rendered using the {@code cygnus:icons} font. + * + * @author theEvilReaper + * @version 1.0.0 + * @since 2.8.0 + */ +public enum GameIcon { + GHOST("󰀀"), + PENTAGRAM("󰀅"), + CLOCK("󰀆"), + PAGE("󰀇"), + FLASHLIGHT("󰀊"), + MAP("󰀋"), + BUILDER("󰀌"); + + /** + * The font key for game and UI icons defined in the resource pack. + */ + public static final Key FONT = Key.key("cygnus", "icons"); + + // Standard role and UI mappings + public static final GameIcon SPECTATOR = GHOST; + public static final GameIcon SURVIVOR = FLASHLIGHT; + public static final GameIcon SLENDER = PENTAGRAM; + public static final GameIcon TIME = CLOCK; + + private final String glyph; + + GameIcon(String glyph) { + this.glyph = glyph; + } + + /** + * Returns the unicode glyph representing this icon. + * + * @return the glyph string + */ + public String glyph() { + return this.glyph; + } + + /** + * Returns the pre-built {@link Component} with the custom icon font. + * + * @return the component + */ + public Component asComponent() { + return Component.text(this.glyph).font(FONT); + } + + /** + * Formats a component by prepending this icon and resetting the font for subsequent text. + * + * @param trailing the trailing component to append after the icon + * @return the combined component with default font reset + */ + public Component format(Component trailing) { + return Component.empty() + .append(asComponent()) + //.append(Component.space().font(RankTag.DEFAULT_MINECRAFT_FONT)) + .append(trailing); + } + + /** + * Formats a text string by prepending this icon and resetting the font for subsequent text. + * + * @param text the text to append after the icon + * @return the combined component with default font reset + */ + public Component format(String text) { + return Component.empty(); + /// return format(Component.text(text).font(RankTag.DEFAULT_MINECRAFT_FONT)); + } + + /** + * Formats multiple icons together, followed by the trailing component with default font reset. + * + * @param trailing the component to append after the icons + * @param icons the icons to prepend + * @return the combined component with default font reset + */ + public static Component formatMultiple(Component trailing, GameIcon... icons) { + Component result = Component.empty(); + for (GameIcon icon : icons) { + result = result.append(icon.asComponent()); + // .append(Component.space().font(RankTag.DEFAULT_MINECRAFT_FONT)); + } + return result.append(trailing); + } +} diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/tag/GameTag.java b/common/src/main/java/net/onelitefeather/cygnus/common/tag/GameTag.java new file mode 100644 index 00000000..3a8c7cba --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/tag/GameTag.java @@ -0,0 +1,82 @@ +package net.onelitefeather.cygnus.common.tag; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +//import net.onelitefeather.cygnus.common.rank.RankTag; + +/** + * Represents custom game and system badges/tags (e.g. Slender Powerline NameTag) + * rendered using the {@code cygnus:tags} font. + * + * @author theEvilReaper + * @version 1.0.0 + * @since 2.8.0 + */ +public enum GameTag { + SLENDER("\uDB80\uDC20"), + MAP_INFO("\uDB80\uDC21"); + + /** + * The font key for game tags defined in the resource pack. + */ + public static final Key FONT = Key.key("cygnus", "tags"); + + private final String glyph; + + GameTag(String glyph) { + this.glyph = glyph; + } + + /** + * Returns the unicode glyph representing this game tag. + * + * @return the glyph string + */ + public String glyph() { + return this.glyph; + } + + /** + * Returns the pre-built {@link Component} with the custom tag font. + * + * @return the component + */ + public Component asComponent() { + return Component.text(this.glyph).font(FONT); + } + + /** + * Returns the pre-built prefix component with font reset. + * + * @return the prefix component + */ + public Component asPrefix() { + return Component.empty() + .append(asComponent()) + .append(Component.space()); + } + + /** + * Formats a component by prepending this tag and resetting the font for subsequent text. + * + * @param trailing the trailing component to append after the prefix + * @return the combined component with default font reset + */ + public Component format(Component trailing) { + return Component.empty() + .append(asComponent()) + // .append(Component.space().font(RankTag.DEFAULT_MINECRAFT_FONT)) + .append(trailing); + } + + /** + * Formats a text string by prepending this tag and resetting the font for subsequent text. + * + * @param text the text to append after the prefix + * @return the combined component with default font reset + */ + public Component format(String text) { + return Component.empty(); + //return format(Component.text(text).font(RankTag.DEFAULT_MINECRAFT_FONT)); + } +} diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/icon/GameIconTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/icon/GameIconTest.java new file mode 100644 index 00000000..9dc70947 --- /dev/null +++ b/common/src/test/java/net/onelitefeather/cygnus/common/icon/GameIconTest.java @@ -0,0 +1,43 @@ +package net.onelitefeather.cygnus.common.icon; + +import net.kyori.adventure.text.Component; +// import net.onelitefeather.cygnus.common.rank.RankTag; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +class GameIconTest { + + @Test + void testGameIconProperties() { + assertEquals("󰀀", GameIcon.GHOST.glyph()); + assertEquals("󰀅", GameIcon.PENTAGRAM.glyph()); + assertEquals("󰀆", GameIcon.CLOCK.glyph()); + assertEquals("󰀇", GameIcon.PAGE.glyph()); + assertEquals("󰀊", GameIcon.FLASHLIGHT.glyph()); + assertEquals("󰀋", GameIcon.MAP.glyph()); + assertEquals("󰀌", GameIcon.BUILDER.glyph()); + + assertSame(GameIcon.GHOST, GameIcon.SPECTATOR); + assertSame(GameIcon.FLASHLIGHT, GameIcon.SURVIVOR); + assertSame(GameIcon.PENTAGRAM, GameIcon.SLENDER); + assertSame(GameIcon.CLOCK, GameIcon.TIME); + + assertEquals(net.kyori.adventure.key.Key.key("cygnus", "icons"), GameIcon.FONT); + assertEquals(GameIcon.FONT, GameIcon.GHOST.asComponent().style().font()); + assertEquals(GameIcon.FONT, GameIcon.FLASHLIGHT.asComponent().style().font()); + assertEquals(GameIcon.FONT, GameIcon.MAP.asComponent().style().font()); + assertEquals(GameIcon.FONT, GameIcon.BUILDER.asComponent().style().font()); + } + + @Disabled("Pending RankTag implementation") + @Test + void testGameIconFormat() { + // Pending RankTag implementation + } +} diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/tag/GameTagTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/tag/GameTagTest.java new file mode 100644 index 00000000..e98028d1 --- /dev/null +++ b/common/src/test/java/net/onelitefeather/cygnus/common/tag/GameTagTest.java @@ -0,0 +1,34 @@ +package net.onelitefeather.cygnus.common.tag; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class GameTagTest { + + @Test + void testSlenderTagFontAndGlyph() { + GameTag tag = GameTag.SLENDER; + assertEquals("\uDB80\uDC20", tag.glyph()); + assertEquals(Key.key("cygnus", "tags"), tag.asComponent().font()); + } + + @Test + void testSlenderPrefix() { + Component prefix = GameTag.SLENDER.asPrefix(); + assertNotNull(prefix); + assertEquals("\uDB80\uDC20 ", PlainTextComponentSerializer.plainText().serialize(prefix)); + } + + @Test + void testMapInfoTag() { + GameTag tag = GameTag.MAP_INFO; + assertEquals("\uDB80\uDC21", tag.glyph()); + assertEquals(Key.key("cygnus", "tags"), tag.asComponent().font()); + } +}