From c4f0256ca03851c7c438eadcb4a738870025d1d8 Mon Sep 17 00:00:00 2001 From: "Blue (Lukas Rieger)" Date: Fri, 7 Aug 2020 14:17:48 +0200 Subject: [PATCH] Implement playermarkers on the web-app --- .../common/plugin/MapUpdateHandler.java | 16 -- .../bluemap/common/plugin/Plugin.java | 6 + .../serverinterface/ServerEventListener.java | 12 +- .../common/plugin/skins/PlayerSkin.java | 152 ++++++++++++++++++ .../plugin/skins/PlayerSkinUpdater.java | 63 ++++++++ .../core/resourcepack/TextureGallery.java | 4 +- .../main/webroot/assets/playerheads/alex.png | Bin 0 -> 2956 bytes .../main/webroot/assets/playerheads/steve.png | Bin 0 -> 3009 bytes .../src/main/webroot/js/libs/BlueMap.js | 3 +- .../main/webroot/js/libs/hud/MarkerManager.js | 54 ++++++- .../src/main/webroot/js/libs/hud/POIMarker.js | 1 - .../main/webroot/js/libs/hud/PlayerMarker.js | 90 +++++++++++ .../webroot/js/libs/hud/PlayerMarkerSet.js | 85 ++++++++++ .../main/webroot/style/modules/hudInfo.scss | 37 ++++- 14 files changed, 490 insertions(+), 33 deletions(-) create mode 100644 BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkin.java create mode 100644 BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/skins/PlayerSkinUpdater.java create mode 100644 BlueMapCore/src/main/webroot/assets/playerheads/alex.png create mode 100644 BlueMapCore/src/main/webroot/assets/playerheads/steve.png create mode 100644 BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarker.js create mode 100644 BlueMapCore/src/main/webroot/js/libs/hud/PlayerMarkerSet.js 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 0000000000000000000000000000000000000000..a49a52174023b120172acc6165e2e33fbf551c5b GIT binary patch literal 2956 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').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