diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/MapUpdateHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/MapUpdateHandler.java index 7d60f966..ac69cd1a 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/MapUpdateHandler.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/MapUpdateHandler.java @@ -35,7 +35,6 @@ import de.bluecolored.bluemap.common.MapType; import de.bluecolored.bluemap.common.RenderManager; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; -import de.bluecolored.bluemap.common.plugin.text.Text; public class MapUpdateHandler implements ServerEventListener { @@ -135,20 +134,5 @@ public void flushTileBuffer() { updateBuffer.clear(); } } - - @Override - public void onPlayerJoin(UUID playerUuid) { - - } - - @Override - public void onPlayerLeave(UUID playerUuid) { - - } - - @Override - public void onChatMessage(Text message) { - - } } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java index ac31bc3c..5bdfff37 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java @@ -51,6 +51,7 @@ import de.bluecolored.bluemap.common.RenderManager; import de.bluecolored.bluemap.common.api.BlueMapAPIImpl; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; +import de.bluecolored.bluemap.common.plugin.skins.PlayerSkinUpdater; import de.bluecolored.bluemap.core.config.ConfigManager; import de.bluecolored.bluemap.core.config.MainConfig; import de.bluecolored.bluemap.core.config.MainConfig.MapConfig; @@ -86,6 +87,7 @@ public class Plugin { private Map maps; private MapUpdateHandler updateHandler; + private PlayerSkinUpdater skinUpdater; private RenderManager renderManager; private BlueMapWebServer webServer; @@ -269,6 +271,10 @@ public synchronized void load() throws IOException, ParseResourceException { this.updateHandler = new MapUpdateHandler(this); serverInterface.registerListener(updateHandler); + //start skin updater + this.skinUpdater = new PlayerSkinUpdater(config.getWebRoot().resolve("assets").resolve("playerheads").toFile()); + serverInterface.registerListener(skinUpdater); + //create/update webfiles WebFilesManager webFilesManager = new WebFilesManager(config.getWebRoot()); if (webFilesManager.needsUpdate()) { diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java index aa530ae1..cbb2a97f 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/serverinterface/ServerEventListener.java @@ -33,16 +33,16 @@ public interface ServerEventListener { - void onWorldSaveToDisk(UUID world); + default void onWorldSaveToDisk(UUID world) {}; - void onBlockChange(UUID world, Vector3i blockPos); + default void onBlockChange(UUID world, Vector3i blockPos) {}; - void onChunkFinishedGeneration(UUID world, Vector2i chunkPos); + default void onChunkFinishedGeneration(UUID world, Vector2i chunkPos) {}; - void onPlayerJoin(UUID playerUuid); + default void onPlayerJoin(UUID playerUuid) {}; - void onPlayerLeave(UUID playerUuid); + default void onPlayerLeave(UUID playerUuid) {}; - void onChatMessage(Text message); + default void onChatMessage(Text message) {}; } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkin.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkin.java new file mode 100644 index 00000000..5201f8dc --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkin.java @@ -0,0 +1,152 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.common.plugin.skins; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.Base64; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.imageio.ImageIO; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import de.bluecolored.bluemap.core.logger.Logger; + +public class PlayerSkin { + + private final UUID uuid; + private long lastUpdate; + + public PlayerSkin(UUID uuid) { + this.uuid = uuid; + this.lastUpdate = -1; + } + + public void update(File storageFolder) { + long now = System.currentTimeMillis(); + if (lastUpdate > 0 && lastUpdate + 600000 > now) return; // only update if skin is older than 10 minutes + + lastUpdate = now; + + new Thread(() -> { + try { + Future futureSkin = loadSkin(); + BufferedImage skin = futureSkin.get(10, TimeUnit.SECONDS); + BufferedImage head = createHead(skin); + ImageIO.write(head, "png", new File(storageFolder, uuid.toString() + ".png")); + } catch (ExecutionException | TimeoutException e) { + Logger.global.logWarning("Failed to load player-skin from mojang-servers: " + e); + } catch (IOException e) { + Logger.global.logError("Failed to write player-head image!", e); + } catch (InterruptedException ignore) {} + }).start(); + } + + public BufferedImage createHead(BufferedImage skinTexture) { + BufferedImage head = new BufferedImage(8, 8, skinTexture.getType()); + + BufferedImage layer1 = skinTexture.getSubimage(8, 8, 8, 8); + BufferedImage layer2 = skinTexture.getSubimage(40, 8, 8, 8); + + try { + Graphics2D g = head.createGraphics(); + g.drawImage(layer1, 0, 0, null); + g.drawImage(layer2, 0, 0, null); + } catch (Throwable t) { // There might be problems with headless servers when loading the graphics class + Logger.global.noFloodWarning("headless-graphics-fail", + "Could not access Graphics2D to render player-skin texture. Try adding '-Djava.awt.headless=true' to your startup flags or ignore this warning."); + + layer1.copyData(head.getRaster()); + } + + return head; + } + + public Future loadSkin() { + CompletableFuture image = new CompletableFuture<>(); + + new Thread(() -> { + try { + JsonParser parser = new JsonParser(); + try (Reader reader = requestProfileJson()) { + String textureInfoJson = readTextureInfoJson(parser.parse(reader)); + String textureUrl = readTextureUrl(parser.parse(textureInfoJson)); + image.complete(ImageIO.read(new URL(textureUrl))); + } + } catch (IOException e) { + image.completeExceptionally(e); + } + }).start(); + + return image; + } + + private Reader requestProfileJson() throws IOException { + URL url = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + this.uuid); + return new InputStreamReader(url.openStream()); + } + + private String readTextureInfoJson(JsonElement json) throws IOException { + try { + JsonArray properties = json.getAsJsonObject().getAsJsonArray("properties"); + + for (JsonElement element : properties) { + if (element.getAsJsonObject().get("name").getAsString().equals("textures")) { + return new String(Base64.getDecoder().decode(element.getAsJsonObject().get("value").getAsString().getBytes())); + } + } + + throw new IOException("No texture info found!"); + } catch (IllegalStateException | ClassCastException e) { + throw new IOException(e); + } + + } + + private String readTextureUrl(JsonElement json) throws IOException { + try { + return json.getAsJsonObject() + .getAsJsonObject("textures") + .getAsJsonObject("SKIN") + .get("url").getAsString(); + } catch (IllegalStateException | ClassCastException e) { + throw new IOException(e); + } + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkinUpdater.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkinUpdater.java new file mode 100644 index 00000000..2332a3be --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkinUpdater.java @@ -0,0 +1,63 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package de.bluecolored.bluemap.common.plugin.skins; + +import java.io.File; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import de.bluecolored.bluemap.common.plugin.serverinterface.ServerEventListener; + +public class PlayerSkinUpdater implements ServerEventListener { + + private File storageFolder; + + private Map skins; + + public PlayerSkinUpdater(File storageFolder) { + this.storageFolder = storageFolder; + this.skins = new ConcurrentHashMap<>(); + + this.storageFolder.mkdirs(); + } + + public void updateSkin(UUID playerUuid) { + PlayerSkin skin = skins.get(playerUuid); + + if (skin == null) { + skin = new PlayerSkin(playerUuid); + skins.put(playerUuid, skin); + } + + skin.update(storageFolder); + } + + @Override + public void onPlayerJoin(UUID playerUuid) { + updateSkin(playerUuid); + } + +} diff --git a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java index 4aa93004..4c6f5afd 100644 --- a/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java +++ b/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/TextureGallery.java @@ -198,9 +198,7 @@ public synchronized Texture loadTexture(FileAccess fileAccess, String path) thro //crop off animation frames if (image.getHeight() > image.getWidth()){ - BufferedImage cropped = new BufferedImage(image.getWidth(), image.getWidth(), image.getType()); - image.copyData(cropped.getRaster()); - image = cropped; + image = image.getSubimage(0, 0, image.getWidth(), image.getWidth()); } //check halfTransparency diff --git a/BlueMapCore/src/main/webroot/assets/playerheads/alex.png b/BlueMapCore/src/main/webroot/assets/playerheads/alex.png new file mode 100644 index 00000000..a49a5217 Binary files /dev/null and b/BlueMapCore/src/main/webroot/assets/playerheads/alex.png differ diff --git a/BlueMapCore/src/main/webroot/assets/playerheads/steve.png b/BlueMapCore/src/main/webroot/assets/playerheads/steve.png new file mode 100644 index 00000000..6037770b Binary files /dev/null and b/BlueMapCore/src/main/webroot/assets/playerheads/steve.png differ diff --git a/BlueMapCore/src/main/webroot/js/libs/BlueMap.js b/BlueMapCore/src/main/webroot/js/libs/BlueMap.js index b0fea56e..235510f7 100644 --- a/BlueMapCore/src/main/webroot/js/libs/BlueMap.js +++ b/BlueMapCore/src/main/webroot/js/libs/BlueMap.js @@ -60,9 +60,10 @@ import { stringToImage, pathFromCoords } from './utils.js'; import {cachePreventionNr, getCookie, setCookie} from "./utils"; export default class BlueMap { - constructor(element, dataRoot) { + constructor(element, dataRoot = "data/", liveApiRoot = "live/") { this.element = $('
').appendTo(element)[0]; this.dataRoot = dataRoot; + this.liveApiRoot = liveApiRoot; this.locationHash = ''; this.cacheSuffix = ''; diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js b/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js index ea722a8e..ec15a1d2 100644 --- a/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js +++ b/BlueMapCore/src/main/webroot/js/libs/hud/MarkerManager.js @@ -3,6 +3,7 @@ import $ from "jquery"; import ToggleButton from "../ui/ToggleButton"; import Label from "../ui/Label"; import {cachePreventionNr} from "../utils"; +import PlayerMarkerSet from "./PlayerMarkerSet"; export default class MarkerManager { @@ -10,14 +11,23 @@ export default class MarkerManager { this.blueMap = blueMap; this.ui = ui; + this.markerData = null; + this.liveData = null; this.markerSets = []; + this.playerMarkerSet = null; + this.readyPromise = - this.loadMarkerData() - .catch(ignore => { - if (this.blueMap.debugInfo) console.debug("Failed load markers:", ignore); - }) - .then(this.loadMarkers); + Promise.all([ + this.loadMarkerData() + .catch(ignore => { + if (this.blueMap.debugInfo) console.debug("Failed load markers:", ignore); + }), + this.checkLiveAPI() + .then(this.initializePlayerMarkers) + ]) + .then(this.loadMarkers) + .then(this.updatePlayerMarkerLoop); $(document).on('bluemap-map-change', this.onBlueMapMapChange); } @@ -41,6 +51,25 @@ export default class MarkerManager { }); } + checkLiveAPI() { + return new Promise((resolve, reject) => { + this.blueMap.fileLoader.load(this.blueMap.liveApiRoot + 'players?' + cachePreventionNr(), + liveData => { + try { + this.liveData = JSON.parse(liveData); + resolve(); + } catch (e){ + reject(e); + } + }, + xhr => {}, + error => { + reject(); + } + ); + }); + } + loadMarkers = () => { if (this.markerData && this.markerData.markerSets) { this.markerData.markerSets.forEach(setData => { @@ -49,12 +78,27 @@ export default class MarkerManager { } }; + initializePlayerMarkers = () => { + if (this.liveData){ + this.playerMarkerSet = new PlayerMarkerSet(this.blueMap); + this.markerSets.push(this.playerMarkerSet); + } + }; + update(){ this.markerSets.forEach(markerSet => { markerSet.update(); }); } + updatePlayerMarkerLoop = () => { + if (this.playerMarkerSet){ + this.playerMarkerSet.updateLive(); + } + + setTimeout(this.updatePlayerMarkerLoop, 2000); + }; + addMenuElements(menu){ let addedLabel = false; this.markerSets.forEach(markerSet => { diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js b/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js index 3a8cdfd4..216f5581 100644 --- a/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js +++ b/BlueMapCore/src/main/webroot/js/libs/hud/POIMarker.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Marker from "./Marker"; import {CSS2DObject} from "./CSS2DRenderer"; -import {Vector3} from "three"; import POI from "../../../assets/poi.svg"; diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarker.js b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarker.js new file mode 100644 index 00000000..1129fa5b --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarker.js @@ -0,0 +1,90 @@ +import $ from 'jquery'; +import Marker from "./Marker"; +import {CSS2DObject} from "./CSS2DRenderer"; + +export default class PlayerMarker extends Marker { + + constructor(blueMap, markerSet, markerData, playerUuid, worldUuid) { + super(blueMap, markerSet, markerData); + + this.online = false; + this.player = playerUuid; + this.world = worldUuid; + + this.animationRunning = false; + this.lastFrame = -1; + } + + setVisible(visible){ + this.visible = visible && this.online && this.world === this.blueMap.settings.maps[this.blueMap.map].world; + + this.blueMap.updateFrame = true; + + if (!this.renderObject){ + let iconElement = $(`
${this.label}
`); + iconElement.find("img").click(this.onClick); + + this.renderObject = new CSS2DObject(iconElement[0]); + this.renderObject.position.copy(this.position); + this.renderObject.onBeforeRender = (renderer, scene, camera) => { + let distanceSquared = this.position.distanceToSquared(camera.position); + if (distanceSquared > 1000000) { + iconElement.addClass("distant"); + } else { + iconElement.removeClass("distant"); + } + + this.updateRenderObject(this.renderObject, scene, camera); + }; + } + + if (this.visible) { + this.blueMap.hudScene.add(this.renderObject); + } else { + this.blueMap.hudScene.remove(this.renderObject); + } + } + + updatePosition = () => { + if (this.renderObject && !this.renderObject.position.equals(this.position)) { + if (this.visible) { + if (!this.animationRunning) { + this.animationRunning = true; + requestAnimationFrame(this.moveAnimation); + } + } else { + this.renderObject.position.copy(this.position); + } + } + }; + + moveAnimation = (time) => { + let delta = time - this.lastFrame; + if (this.lastFrame === -1){ + delta = 20; + } + this.lastFrame = time; + + if (this.renderObject && !this.renderObject.position.equals(this.position)) { + this.renderObject.position.x += (this.position.x - this.renderObject.position.x) * 0.01 * delta; + this.renderObject.position.y += (this.position.y - this.renderObject.position.y) * 0.01 * delta; + this.renderObject.position.z += (this.position.z - this.renderObject.position.z) * 0.01 * delta; + + if (this.renderObject.position.distanceToSquared(this.position) < 0.001) { + this.renderObject.position.copy(this.position); + } + + this.blueMap.updateFrame = true; + + requestAnimationFrame(this.moveAnimation); + } else { + this.animationRunning = false; + this.lastFrame = -1; + } + }; + + onClick = () => { + + } + +} \ No newline at end of file diff --git a/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarkerSet.js b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarkerSet.js new file mode 100644 index 00000000..9668af24 --- /dev/null +++ b/BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarkerSet.js @@ -0,0 +1,85 @@ +import POIMarker from "./POIMarker"; +import ShapeMarker from "./ShapeMarker"; +import {cachePreventionNr} from "../utils"; +import PlayerMarker from "./PlayerMarker"; +import {Vector3} from "three"; + +export default class PlayerMarkerSet { + + constructor(blueMap) { + this.blueMap = blueMap; + this.id = "bluemap-live-players"; + this.label = "players"; + this.toggleable = true; + this.defaultHide = false; + this.marker = []; + this.markerMap = {}; + + this.visible = true; + } + + update() { + this.marker.forEach(marker => { + marker.setVisible(this.visible); + }); + } + + async updateLive(){ + await new Promise((resolve, reject) => { + this.blueMap.fileLoader.load(this.blueMap.liveApiRoot + 'players?' + cachePreventionNr(), + liveData => { + try { + liveData = JSON.parse(liveData); + resolve(liveData); + } catch (e){ + reject(e); + } + }, + xhr => {}, + error => { + reject(error); + } + ); + }).then((liveData) => { + this.updateWith(liveData) + }).catch((e) => { + console.error("Failed to update player-markers!", e); + }); + } + + updateWith(liveData){ + this.marker.forEach(marker => { + marker.nowOnline = false; + }); + + for(let i = 0; i < liveData.players.length; i++){ + let player = liveData.players[i]; + let marker = this.markerMap[player.uuid]; + + if (!marker){ + marker = new PlayerMarker(this.blueMap, this, { + type: "playermarker", + map: null, + position: player.position, + label: player.name, + link: null, + newTab: false + }, player.uuid, player.world); + + this.markerMap[player.uuid] = marker; + this.marker.push(marker); + } + + marker.nowOnline = true; + marker.position = new Vector3(player.position.x, player.position.y + 1.5, player.position.z); + marker.updatePosition(); + } + + this.marker.forEach(marker => { + if (marker.nowOnline !== marker.online){ + marker.online = marker.nowOnline; + marker.setVisible(this.visible); + } + }); + } +} \ No newline at end of file diff --git a/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss b/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss index 997cd8ca..cfdf07e7 100644 --- a/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss +++ b/BlueMapCore/src/main/webroot/style/modules/hudInfo.scss @@ -76,7 +76,7 @@ } } -.bluemap-container .marker-poi { +.bluemap-container .marker-poi, .bluemap-container .marker-player { pointer-events: none; > * { @@ -86,4 +86,39 @@ > img { filter: drop-shadow(1px 1px 3px #0008); } +} + +.bluemap-container .marker-player { + img { + width: 32px; + height: 32px; + + image-rendering: pixelated; + image-rendering: crisp-edges; + + transition: all 0.3s; + } + + .nameplate { + position: absolute; + left: 50%; + top: 0; + transform: translate(-50%, -110%); + background: rgba(50, 50, 50, 0.75); + padding: 0.2em 0.3em; + color: white; + + transition: all 0.3s; + } + + &.distant { + img { + width: 16px; + height: 16px; + } + + .nameplate { + opacity: 0; + } + } } \ No newline at end of file