Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import net.onelitefeather.cygnus.player.event.SpectatorAddEvent;
import net.onelitefeather.cygnus.player.listener.SpectatorAddListener;
import net.onelitefeather.cygnus.player.listener.SpectatorItemListener;
import net.onelitefeather.cygnus.team.RoleIcon;
import net.onelitefeather.cygnus.team.TeamHelper;
import net.onelitefeather.cygnus.utils.Items;
import net.onelitefeather.cygnus.visibility.VisibilityRules;
Expand Down Expand Up @@ -90,7 +91,8 @@ 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));
Component name = Component.text(player.getUsername(), NamedTextColor.GRAY, TextDecoration.STRIKETHROUGH);
player.setDisplayName(RoleIcon.SPECTATOR.prefix(name));
}

/**
Expand Down
84 changes: 84 additions & 0 deletions game/src/main/java/net/onelitefeather/cygnus/team/RoleIcon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package net.onelitefeather.cygnus.team;

import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.onelitefeather.cygnus.common.config.GameConfig;

/**
* Maps each game role to the tab list icon the {@code cygnus:icons} resource pack font provides.
* <p>
* The glyph is rendered {@link NamedTextColor#WHITE} because the pack's icons are full-color bitmaps
* rather than the grayscale masks vanilla glyphs use - any other color would tint the artwork instead
* of leaving it as designed.
* </p>
*
* @author theEvilReaper
* @version 1.0.0
* @since 1.0.0
*/
public enum RoleIcon {

SLENDER(GameConfig.SLENDER_KEY, 0xF0005),
SURVIVOR(GameConfig.SURVIVOR_KEY, 0xF000A),
SPECTATOR(GameConfig.SPECTATOR_KEY, 0xF0000);

private final Key roleKey;
private final Component glyph;

RoleIcon(Key roleKey, int codepoint) {
this.roleKey = roleKey;
this.glyph = Component.text(new String(Character.toChars(codepoint)), NamedTextColor.WHITE).font(iconFont());
}

/**
* Returns the {@code cygnus:icons} resource pack font every {@link #glyph()} is drawn from.
* <p>
* A method rather than a static field: enum constants are initialized before the class's other
* static fields, so a static field here would not yet be set while the constants above are built.
* </p>
*
* @return the font key
*/
private static Key iconFont() {
return Key.key("cygnus", "icons");
}

/**
* Returns the {@link GameConfig} role key this icon represents.
*
* @return the role key
*/
public Key roleKey() {
return roleKey;
}

/**
* Returns the styled glyph component for this role.
*
* @return the icon component
*/
public Component glyph() {
return glyph;
}

/**
* Prepends this role's icon and a space in front of the given name.
* <p>
* The icon has to be appended as a child of a plain, font-less root rather than used as the root
* itself: Adventure components inherit style from their parent, so a root carrying
* {@code font(cygnus:icons)} would leak that font onto the space and name appended after it,
* which has no letter glyphs and renders them as missing-character boxes in the client.
* </p>
*
* @param name the name component to prefix
* @return the icon followed by a space and the given name
*/
public Component prefix(Component name) {
return Component.text()
.append(glyph)
.appendSpace()
.append(name)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,12 @@ public static void updateTabList(TeamService teamService) {
}

slenderTeam.getPlayers().forEach(player -> {
Component slenderDisplayName = Component.text("⛧ ", NamedTextColor.RED)
.append(Component.text(player.getUsername(), NamedTextColor.GRAY));
Component slenderDisplayName = RoleIcon.SLENDER.prefix(Component.text(player.getUsername(), NamedTextColor.GRAY));
player.setDisplayName(slenderDisplayName);
});

survivorTeam.getPlayers().forEach(player -> {
Component survivorDisplayName = Component.text(player.getUsername(), NamedTextColor.GREEN);
Component survivorDisplayName = RoleIcon.SURVIVOR.prefix(Component.text(player.getUsername(), NamedTextColor.GREEN));
player.setDisplayName(survivorDisplayName);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import net.onelitefeather.cygnus.common.Tags;
import net.onelitefeather.cygnus.common.config.GameConfig;
import net.onelitefeather.cygnus.player.CygnusPlayer;
import net.onelitefeather.cygnus.team.RoleIcon;
import net.theevilreaper.xerus.api.team.Team;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -188,9 +189,9 @@ void testJoinStrikesTheTabListNameThrough(@NotNull Env env) {
Component displayName = player.getDisplayName();
assertNotNull(displayName, "a spectator needs a display name to show up in the tab list");
assertEquals(
Component.text(player.getUsername(), NamedTextColor.GRAY, TextDecoration.STRIKETHROUGH),
RoleIcon.SPECTATOR.prefix(Component.text(player.getUsername(), NamedTextColor.GRAY, TextDecoration.STRIKETHROUGH)),
displayName,
"a spectator must be struck through in gray instead of keeping the green survivor name"
"a spectator must show the spectator icon and be struck through in gray instead of keeping the green survivor name"
);

env.destroyInstance(instance, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package net.onelitefeather.cygnus.team;

import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.onelitefeather.cygnus.common.config.GameConfig;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class RoleIconTest {

private static final Key ICON_FONT = Key.key("cygnus", "icons");

@Test
void testSlenderGlyph() {
assertGlyph(RoleIcon.SLENDER, GameConfig.SLENDER_KEY, 0xF0005);
}

@Test
void testSurvivorGlyph() {
assertGlyph(RoleIcon.SURVIVOR, GameConfig.SURVIVOR_KEY, 0xF000A);
}

@Test
void testSpectatorGlyph() {
assertGlyph(RoleIcon.SPECTATOR, GameConfig.SPECTATOR_KEY, 0xF0000);
}

@Test
void testPrefixKeepsIconAndAppendsGivenName() {
Component name = Component.text("theEvilReaper", NamedTextColor.GREEN);
Component prefixed = RoleIcon.SURVIVOR.prefix(name);

assertNull(prefixed.style().font(), "the icon font must not leak onto the space and name, or the client shows missing-glyph boxes for them");
assertEquals(3, prefixed.children().size());
assertEquals(RoleIcon.SURVIVOR.glyph(), prefixed.children().get(0));
assertEquals(Component.space(), prefixed.children().get(1));
assertEquals(name, prefixed.children().get(2));

String plainText = PlainTextComponentSerializer.plainText().serialize(prefixed);
assertTrue(plainText.endsWith(" theEvilReaper"));
}

private void assertGlyph(RoleIcon icon, Key expectedRoleKey, int expectedCodepoint) {
assertEquals(expectedRoleKey, icon.roleKey());
assertEquals(ICON_FONT, icon.glyph().style().font());
assertEquals(NamedTextColor.WHITE, icon.glyph().style().color());

String expectedGlyph = new String(Character.toChars(expectedCodepoint));
assertEquals(expectedGlyph, PlainTextComponentSerializer.plainText().serialize(icon.glyph()));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.onelitefeather.cygnus.utils;

import net.onelitefeather.cygnus.team.RoleIcon;
import net.onelitefeather.cygnus.team.TeamHelper;
import net.theevilreaper.xerus.api.team.Team;
import net.theevilreaper.xerus.api.team.TeamService;
Expand Down Expand Up @@ -33,6 +34,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -171,13 +173,19 @@ void testUpdateTabList(@NotNull Env env) {

Component displayName = player.getDisplayName();
assertNotNull(displayName);
assertTrue(PlainTextComponentSerializer.plainText().serialize(displayName).contains("⛧"));
assertNull(displayName.style().font(), "the icon font must not leak onto the whole name, or the client shows missing-glyph boxes");
assertEquals(RoleIcon.SLENDER.glyph(), displayName.children().get(0));
assertTrue(PlainTextComponentSerializer.plainText().serialize(displayName).endsWith(player.getUsername()));

survivorTeam.getPlayers().forEach(survivor -> {
Component survivorDisplayName = survivor.getDisplayName();
assertNotNull(survivorDisplayName);
assertTrue(survivorDisplayName.hasStyling());
assertEquals(NamedTextColor.GREEN, survivorDisplayName.style().color());
assertNull(survivorDisplayName.style().font(), "the icon font must not leak onto the whole name, or the client shows missing-glyph boxes");
assertEquals(RoleIcon.SURVIVOR.glyph(), survivorDisplayName.children().get(0));

Component nameComponent = survivorDisplayName.children().get(2);
assertTrue(nameComponent.hasStyling());
assertEquals(NamedTextColor.GREEN, nameComponent.style().color());
});

survivorTeam.removePlayers(survivors, Entity::remove);
Expand Down
Loading