- Updated docker-compose.dev.yml to use Dockerfile.dev for backend builds and added HOST environment variable for frontend. - Introduced Dockerfile.dev for streamlined backend development with Go. - Enhanced development documentation to reflect changes in local setup and API proxying. - Removed outdated frontend Dockerfile and adjusted frontend configuration for improved development experience.
531 lines
18 KiB
Vue
531 lines
18 KiB
Vue
<template>
|
|
<div class="relative h-full w-full" @click="(e: MouseEvent) => e.button === 0 && mapLogic.closeContextMenus()">
|
|
<div
|
|
v-if="mapsLoaded && maps.length === 0"
|
|
class="absolute inset-0 z-[500] flex flex-col items-center justify-center gap-4 bg-base-200/90 p-6"
|
|
>
|
|
<p class="text-center text-lg">Map list is empty.</p>
|
|
<p class="text-center text-sm opacity-80">
|
|
Make sure you are logged in and at least one map exists in Admin (uncheck «Hidden» if needed).
|
|
</p>
|
|
<div class="flex gap-2">
|
|
<NuxtLink to="/login" class="btn btn-sm">Login</NuxtLink>
|
|
<NuxtLink to="/admin" class="btn btn-sm btn-primary">Admin</NuxtLink>
|
|
</div>
|
|
</div>
|
|
<div ref="mapRef" class="map h-full w-full" />
|
|
<MapMapCoordsDisplay
|
|
:mapid="mapLogic.state.mapid"
|
|
:display-coords="mapLogic.state.displayCoords"
|
|
/>
|
|
<MapControls
|
|
:show-grid-coordinates="mapLogic.state.showGridCoordinates"
|
|
@update:show-grid-coordinates="(v) => (mapLogic.state.showGridCoordinates.value = v)"
|
|
:hide-markers="mapLogic.state.hideMarkers"
|
|
@update:hide-markers="(v) => (mapLogic.state.hideMarkers.value = v)"
|
|
:selected-map-id="mapLogic.state.selectedMapId.value"
|
|
@update:selected-map-id="(v) => (mapLogic.state.selectedMapId.value = v)"
|
|
:overlay-map-id="mapLogic.state.overlayMapId.value"
|
|
@update:overlay-map-id="(v) => (mapLogic.state.overlayMapId.value = v)"
|
|
:selected-marker-id="mapLogic.state.selectedMarkerId.value"
|
|
@update:selected-marker-id="(v) => (mapLogic.state.selectedMarkerId.value = v)"
|
|
:selected-player-id="mapLogic.state.selectedPlayerId.value"
|
|
@update:selected-player-id="(v) => (mapLogic.state.selectedPlayerId.value = v)"
|
|
:maps="maps"
|
|
:quest-givers="questGivers"
|
|
:players="players"
|
|
@zoom-in="mapLogic.zoomIn(map)"
|
|
@zoom-out="mapLogic.zoomOutControl(map)"
|
|
@reset-view="mapLogic.resetView(map)"
|
|
/>
|
|
<MapMapContextMenu
|
|
:context-menu="mapLogic.contextMenu"
|
|
@wipe-tile="onWipeTile"
|
|
@rewrite-coords="onRewriteCoords"
|
|
@hide-marker="onHideMarker"
|
|
/>
|
|
<MapMapCoordSetModal
|
|
:coord-set-from="mapLogic.coordSetFrom"
|
|
:coord-set="mapLogic.coordSet"
|
|
:open="mapLogic.coordSetModalOpen"
|
|
@close="mapLogic.closeCoordSetModal()"
|
|
@submit="onSubmitCoordSet"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import MapControls from '~/components/map/MapControls.vue'
|
|
import { GridCoordLayer, HnHCRS, HnHDefaultZoom, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
|
import { SmartTileLayer } from '~/lib/SmartTileLayer'
|
|
import { Marker } from '~/lib/Marker'
|
|
import { Character } from '~/lib/Character'
|
|
import { UniqueList } from '~/lib/UniqueList'
|
|
import type L from 'leaflet'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
characterId?: number
|
|
mapId?: number
|
|
gridX?: number
|
|
gridY?: number
|
|
zoom?: number
|
|
}>(),
|
|
{ characterId: -1, mapId: undefined, gridX: 0, gridY: 0, zoom: 1 }
|
|
)
|
|
|
|
const mapRef = ref<HTMLElement | null>(null)
|
|
const api = useMapApi()
|
|
const mapLogic = useMapLogic()
|
|
|
|
const maps = ref<{ ID: number; Name: string; size?: number }[]>([])
|
|
const mapsLoaded = ref(false)
|
|
const questGivers = ref<{ id: number; name: string; marker?: any }[]>([])
|
|
const players = ref<{ id: number; name: string }[]>([])
|
|
const auths = ref<string[]>([])
|
|
|
|
let map: L.Map | null = null
|
|
let layer: InstanceType<typeof SmartTileLayer> | null = null
|
|
let overlayLayer: InstanceType<typeof SmartTileLayer> | null = null
|
|
let coordLayer: InstanceType<typeof GridCoordLayer> | null = null
|
|
let markerLayer: L.LayerGroup | null = null
|
|
let source: EventSource | null = null
|
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
let markers: UniqueList<InstanceType<typeof Marker>> | null = null
|
|
let characters: UniqueList<InstanceType<typeof Character>> | null = null
|
|
let markersHidden = false
|
|
let autoMode = false
|
|
let mapContainer: HTMLElement | null = null
|
|
let contextMenuHandler: ((ev: MouseEvent) => void) | null = null
|
|
|
|
function toLatLng(x: number, y: number) {
|
|
return map!.unproject([x, y], HnHMaxZoom)
|
|
}
|
|
|
|
function changeMap(id: number) {
|
|
if (id === mapLogic.state.mapid.value) return
|
|
mapLogic.state.mapid.value = id
|
|
mapLogic.state.selectedMapId.value = id
|
|
if (layer) {
|
|
layer.map = id
|
|
layer.redraw()
|
|
}
|
|
if (overlayLayer) {
|
|
overlayLayer.map = -1
|
|
overlayLayer.redraw()
|
|
}
|
|
if (markers && !markersHidden) {
|
|
markers.getElements().forEach((it: any) => it.remove({ map: map!, markerLayer: markerLayer!, mapid: id }))
|
|
markers.getElements().filter((it: any) => it.map === id).forEach((it: any) => it.add({ map: map!, markerLayer: markerLayer!, mapid: id }))
|
|
}
|
|
if (characters) {
|
|
characters.getElements().forEach((it: any) => {
|
|
it.remove({ map: map! })
|
|
it.add({ map: map!, mapid: id })
|
|
})
|
|
}
|
|
}
|
|
|
|
function onWipeTile(coords: { x: number; y: number } | undefined) {
|
|
if (!coords) return
|
|
mapLogic.closeContextMenus()
|
|
api.adminWipeTile({ map: mapLogic.state.mapid.value, x: coords.x, y: coords.y })
|
|
}
|
|
|
|
function onRewriteCoords(coords: { x: number; y: number } | undefined) {
|
|
if (!coords) return
|
|
mapLogic.closeContextMenus()
|
|
mapLogic.openCoordSet(coords)
|
|
}
|
|
|
|
function onHideMarker(id: number | undefined) {
|
|
if (id == null) return
|
|
mapLogic.closeContextMenus()
|
|
api.adminHideMarker({ id })
|
|
const m = markers?.byId(id)
|
|
if (m) m.remove({ map: map!, markerLayer: markerLayer!, mapid: mapLogic.state.mapid.value })
|
|
}
|
|
|
|
function onSubmitCoordSet(from: { x: number; y: number }, to: { x: number; y: number }) {
|
|
api.adminSetCoords({
|
|
map: mapLogic.state.mapid.value,
|
|
fx: from.x,
|
|
fy: from.y,
|
|
tx: to.x,
|
|
ty: to.y,
|
|
})
|
|
mapLogic.closeCoordSetModal()
|
|
}
|
|
|
|
function onKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') mapLogic.closeContextMenus()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (import.meta.client) {
|
|
window.addEventListener('keydown', onKeydown)
|
|
}
|
|
if (!import.meta.client || !mapRef.value) return
|
|
const L = (await import('leaflet')).default
|
|
|
|
const [charactersData, mapsData] = await Promise.all([
|
|
api.getCharacters().then((d) => (Array.isArray(d) ? d : [])).catch(() => []),
|
|
api.getMaps().then((d) => (d && typeof d === 'object' ? d : {})).catch(() => ({})),
|
|
])
|
|
|
|
const mapsList: { ID: number; Name: string; size?: number }[] = []
|
|
const raw = mapsData as Record<string, { ID?: number; Name?: string; id?: number; name?: string; size?: number }>
|
|
for (const id in raw) {
|
|
const m = raw[id]
|
|
if (!m || typeof m !== 'object') continue
|
|
const idVal = m.ID ?? m.id
|
|
const nameVal = m.Name ?? m.name
|
|
if (idVal == null || nameVal == null) continue
|
|
mapsList.push({ ID: Number(idVal), Name: String(nameVal), size: m.size })
|
|
}
|
|
mapsList.sort((a, b) => (b.size ?? 0) - (a.size ?? 0))
|
|
maps.value = mapsList
|
|
mapsLoaded.value = true
|
|
|
|
const config = (await api.getConfig().catch(() => ({}))) as { title?: string; auths?: string[] }
|
|
if (config?.title) document.title = config.title
|
|
const user = await api.me().catch(() => null)
|
|
auths.value = (user as { auths?: string[] } | null)?.auths ?? config?.auths ?? []
|
|
|
|
map = L.map(mapRef.value, {
|
|
minZoom: HnHMinZoom,
|
|
maxZoom: HnHMaxZoom,
|
|
crs: HnHCRS,
|
|
attributionControl: false,
|
|
zoomControl: false,
|
|
inertia: true,
|
|
zoomAnimation: true,
|
|
fadeAnimation: true,
|
|
markerZoomAnimation: true,
|
|
})
|
|
|
|
const initialMapId =
|
|
props.mapId != null && props.mapId >= 1 ? props.mapId : mapsList.length > 0 ? (mapsList[0]?.ID ?? 0) : 0
|
|
mapLogic.state.mapid.value = initialMapId
|
|
|
|
const runtimeConfig = useRuntimeConfig()
|
|
const apiBase = (runtimeConfig.public.apiBase as string) ?? '/map/api'
|
|
const backendBase = apiBase.replace(/\/api\/?$/, '') || '/map'
|
|
const tileUrl = `${backendBase}/grids/{map}/{z}/{x}_{y}.png?{cache}`
|
|
layer = new SmartTileLayer(tileUrl, {
|
|
minZoom: 1,
|
|
maxZoom: 6,
|
|
zoomOffset: 0,
|
|
zoomReverse: true,
|
|
tileSize: TileSize,
|
|
updateWhenIdle: true,
|
|
keepBuffer: 2,
|
|
}) as any
|
|
layer!.map = initialMapId
|
|
layer!.invalidTile =
|
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
|
|
layer!.addTo(map)
|
|
|
|
overlayLayer = new SmartTileLayer(tileUrl, {
|
|
minZoom: 1,
|
|
maxZoom: 6,
|
|
zoomOffset: 0,
|
|
zoomReverse: true,
|
|
tileSize: TileSize,
|
|
opacity: 0.5,
|
|
updateWhenIdle: true,
|
|
keepBuffer: 2,
|
|
}) as any
|
|
overlayLayer!.map = -1
|
|
overlayLayer!.invalidTile =
|
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
|
overlayLayer!.addTo(map)
|
|
|
|
coordLayer = new GridCoordLayer({
|
|
tileSize: TileSize,
|
|
minZoom: HnHMinZoom,
|
|
maxZoom: HnHMaxZoom,
|
|
opacity: 0,
|
|
visible: false,
|
|
pane: 'tilePane',
|
|
} as any)
|
|
coordLayer.addTo(map)
|
|
coordLayer.setZIndex(500)
|
|
|
|
markerLayer = L.layerGroup()
|
|
markerLayer.addTo(map)
|
|
markerLayer.setZIndex(600)
|
|
|
|
const baseURL = useRuntimeConfig().app.baseURL ?? '/'
|
|
const markerIconPath = baseURL.endsWith('/') ? baseURL : baseURL + '/'
|
|
L.Icon.Default.imagePath = markerIconPath
|
|
|
|
// Document-level capture so we get contextmenu before any map layer or iframe can swallow it
|
|
mapContainer = map.getContainer()
|
|
contextMenuHandler = (ev: MouseEvent) => {
|
|
const target = ev.target as Node
|
|
if (!mapContainer?.contains(target)) return
|
|
const isAdmin = auths.value.includes('admin')
|
|
if (import.meta.dev) console.log('[MapView contextmenu]', { isAdmin, auths: auths.value })
|
|
if (isAdmin) {
|
|
ev.preventDefault()
|
|
ev.stopPropagation()
|
|
const rect = mapContainer.getBoundingClientRect()
|
|
const containerPoint = L.point(ev.clientX - rect.left, ev.clientY - rect.top)
|
|
const latlng = map!.containerPointToLatLng(containerPoint)
|
|
const point = map!.project(latlng, 6)
|
|
const coords = { x: Math.floor(point.x / TileSize), y: Math.floor(point.y / TileSize) }
|
|
mapLogic.openTileContextMenu(ev.clientX, ev.clientY, coords)
|
|
}
|
|
}
|
|
document.addEventListener('contextmenu', contextMenuHandler, true)
|
|
|
|
const updatesPath = `${backendBase}/updates`
|
|
const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
|
|
source = new EventSource(updatesUrl)
|
|
source.onmessage = (event: MessageEvent) => {
|
|
try {
|
|
const raw = event?.data
|
|
if (raw == null || typeof raw !== 'string' || raw.trim() === '') return
|
|
const updates = JSON.parse(raw)
|
|
if (!Array.isArray(updates)) return
|
|
for (const u of updates) {
|
|
const key = `${u.M}:${u.X}:${u.Y}:${u.Z}`
|
|
layer!.cache[key] = u.T
|
|
if (overlayLayer) overlayLayer.cache[key] = u.T
|
|
if (layer!.map === u.M) layer!.refresh(u.X, u.Y, u.Z)
|
|
if (overlayLayer && overlayLayer.map === u.M) overlayLayer.refresh(u.X, u.Y, u.Z)
|
|
}
|
|
} catch {
|
|
// Ignore parse errors
|
|
}
|
|
}
|
|
source.onerror = () => {}
|
|
source.addEventListener('merge', (e: MessageEvent) => {
|
|
try {
|
|
const merge = JSON.parse(e?.data ?? '{}')
|
|
if (mapLogic.state.mapid.value === merge.From) {
|
|
const mapTo = merge.To
|
|
const point = map!.project(map!.getCenter(), 6)
|
|
const coordinate = {
|
|
x: Math.floor(point.x / TileSize) + merge.Shift.x,
|
|
y: Math.floor(point.y / TileSize) + merge.Shift.y,
|
|
z: map!.getZoom(),
|
|
}
|
|
const latLng = toLatLng(coordinate.x * 100, coordinate.y * 100)
|
|
changeMap(mapTo)
|
|
api.getMarkers().then((body) => updateMarkers(Array.isArray(body) ? body : []))
|
|
map!.setView(latLng, map!.getZoom())
|
|
}
|
|
} catch {
|
|
// Ignore merge parse errors
|
|
}
|
|
})
|
|
|
|
markers = new UniqueList<InstanceType<typeof Marker>>()
|
|
characters = new UniqueList<InstanceType<typeof Character>>()
|
|
|
|
updateCharacters(charactersData as any[])
|
|
|
|
if (props.characterId !== undefined && props.characterId >= 0) {
|
|
mapLogic.state.trackingCharacterId.value = props.characterId
|
|
} else if (props.mapId != null && props.gridX != null && props.gridY != null && props.zoom != null) {
|
|
const latLng = toLatLng(props.gridX * 100, props.gridY * 100)
|
|
if (mapLogic.state.mapid.value !== props.mapId) changeMap(props.mapId)
|
|
mapLogic.state.selectedMapId.value = props.mapId
|
|
map.setView(latLng, props.zoom, { animate: false })
|
|
} else if (mapsList.length > 0) {
|
|
const first = mapsList[0]
|
|
if (first) {
|
|
changeMap(first.ID)
|
|
mapLogic.state.selectedMapId.value = first.ID
|
|
map.setView([0, 0], HnHDefaultZoom, { animate: false })
|
|
}
|
|
}
|
|
|
|
nextTick(() => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
if (map) map.invalidateSize()
|
|
})
|
|
})
|
|
})
|
|
|
|
intervalId = setInterval(() => {
|
|
api.getCharacters().then((body) => updateCharacters(Array.isArray(body) ? body : [])).catch(() => clearInterval(intervalId!))
|
|
}, 2000)
|
|
|
|
api.getMarkers().then((body) => updateMarkers(Array.isArray(body) ? body : []))
|
|
|
|
function updateMarkers(markersData: any[]) {
|
|
if (!markers || !map || !markerLayer) return
|
|
const list = Array.isArray(markersData) ? markersData : []
|
|
const ctx = { map, markerLayer, mapid: mapLogic.state.mapid.value, overlayLayer, auths: auths.value }
|
|
markers.update(
|
|
list.map((it) => new Marker(it)),
|
|
(marker: InstanceType<typeof Marker>) => {
|
|
if (marker.map === mapLogic.state.mapid.value || marker.map === overlayLayer?.map) marker.add(ctx)
|
|
marker.setClickCallback(() => map!.setView(marker.marker!.getLatLng(), HnHMaxZoom))
|
|
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
|
|
if (auths.value.includes('admin')) {
|
|
mev.originalEvent.preventDefault()
|
|
mev.originalEvent.stopPropagation()
|
|
mapLogic.openMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
|
|
}
|
|
})
|
|
},
|
|
(marker: InstanceType<typeof Marker>) => marker.remove(ctx),
|
|
(marker: InstanceType<typeof Marker>, updated: any) => marker.update(ctx, updated)
|
|
)
|
|
questGivers.value = markers.getElements().filter((it: any) => it.type === 'quest')
|
|
}
|
|
|
|
function updateCharacters(charactersData: any[]) {
|
|
if (!characters || !map) return
|
|
const list = Array.isArray(charactersData) ? charactersData : []
|
|
const ctx = { map, mapid: mapLogic.state.mapid.value }
|
|
characters.update(
|
|
list.map((it) => new Character(it)),
|
|
(character: InstanceType<typeof Character>) => {
|
|
character.add(ctx)
|
|
character.setClickCallback(() => (mapLogic.state.trackingCharacterId.value = character.id))
|
|
},
|
|
(character: InstanceType<typeof Character>) => character.remove(ctx),
|
|
(character: InstanceType<typeof Character>, updated: any) => {
|
|
if (mapLogic.state.trackingCharacterId.value === updated.id) {
|
|
if (mapLogic.state.mapid.value !== updated.map) changeMap(updated.map)
|
|
const latlng = map!.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
|
map!.setView(latlng, HnHMaxZoom)
|
|
}
|
|
character.update(ctx, updated)
|
|
}
|
|
)
|
|
players.value = characters.getElements()
|
|
}
|
|
|
|
watch(mapLogic.state.showGridCoordinates, (v) => {
|
|
if (coordLayer) {
|
|
;(coordLayer.options as { visible?: boolean }).visible = v
|
|
coordLayer.setOpacity(v ? 1 : 0)
|
|
if (v && map) {
|
|
coordLayer.bringToFront?.()
|
|
coordLayer.redraw?.()
|
|
map.invalidateSize()
|
|
} else if (coordLayer) {
|
|
coordLayer.redraw?.()
|
|
}
|
|
}
|
|
})
|
|
|
|
watch(mapLogic.state.hideMarkers, (v) => {
|
|
markersHidden = v
|
|
if (!markers) return
|
|
const ctx = { map: map!, markerLayer: markerLayer!, mapid: mapLogic.state.mapid.value, overlayLayer }
|
|
if (v) {
|
|
markers.getElements().forEach((it: any) => it.remove(ctx))
|
|
} else {
|
|
markers.getElements().forEach((it: any) => it.remove(ctx))
|
|
markers.getElements().filter((it: any) => it.map === mapLogic.state.mapid.value || it.map === overlayLayer?.map).forEach((it: any) => it.add(ctx))
|
|
}
|
|
})
|
|
|
|
watch(mapLogic.state.trackingCharacterId, (value) => {
|
|
if (value === -1) return
|
|
const character = characters?.byId(value)
|
|
if (character) {
|
|
changeMap(character.map)
|
|
const latlng = map!.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
|
map!.setView(latlng, HnHMaxZoom)
|
|
autoMode = true
|
|
} else {
|
|
map!.setView([0, 0], HnHMinZoom)
|
|
mapLogic.state.trackingCharacterId.value = -1
|
|
}
|
|
})
|
|
|
|
watch(mapLogic.state.selectedMapId, (value) => {
|
|
if (value == null) return
|
|
changeMap(value)
|
|
const zoom = map!.getZoom()
|
|
map!.setView([0, 0], zoom)
|
|
})
|
|
|
|
watch(mapLogic.state.overlayMapId, (value) => {
|
|
if (overlayLayer) overlayLayer.map = value ?? -1
|
|
overlayLayer?.redraw()
|
|
if (!markers) return
|
|
const ctx = { map: map!, markerLayer: markerLayer!, mapid: mapLogic.state.mapid.value, overlayLayer }
|
|
markers.getElements().forEach((it: any) => it.remove(ctx))
|
|
markers.getElements().filter((it: any) => it.map === mapLogic.state.mapid.value || it.map === (value ?? -1)).forEach((it: any) => it.add(ctx))
|
|
})
|
|
|
|
watch(mapLogic.state.selectedMarkerId, (value) => {
|
|
if (value == null) return
|
|
const marker = markers?.byId(value)
|
|
if (marker?.marker) map!.setView(marker.marker.getLatLng(), map!.getZoom())
|
|
})
|
|
|
|
watch(mapLogic.state.selectedPlayerId, (value) => {
|
|
if (value != null) mapLogic.state.trackingCharacterId.value = value
|
|
})
|
|
|
|
map.on('moveend', () => mapLogic.updateDisplayCoords(map))
|
|
mapLogic.updateDisplayCoords(map)
|
|
map.on('zoomend', () => {
|
|
if (map) map.invalidateSize()
|
|
})
|
|
map.on('drag', () => {
|
|
mapLogic.state.trackingCharacterId.value = -1
|
|
})
|
|
map.on('zoom', () => {
|
|
if (autoMode) {
|
|
autoMode = false
|
|
} else {
|
|
mapLogic.state.trackingCharacterId.value = -1
|
|
}
|
|
})
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (import.meta.client) {
|
|
window.removeEventListener('keydown', onKeydown)
|
|
}
|
|
if (contextMenuHandler) {
|
|
document.removeEventListener('contextmenu', contextMenuHandler, true)
|
|
}
|
|
if (intervalId) clearInterval(intervalId)
|
|
if (source) source.close()
|
|
if (map) map.remove()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.map {
|
|
height: 100%;
|
|
min-height: 400px;
|
|
}
|
|
:deep(.leaflet-container .leaflet-tile-pane img.leaflet-tile) {
|
|
mix-blend-mode: normal;
|
|
visibility: visible !important;
|
|
}
|
|
:deep(.map-tile) {
|
|
width: 100px;
|
|
height: 100px;
|
|
min-width: 100px;
|
|
min-height: 100px;
|
|
position: relative;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
|
color: #fff;
|
|
font-size: 11px;
|
|
line-height: 1.2;
|
|
pointer-events: none;
|
|
text-shadow: 0 0 2px #000, 0 1px 2px #000;
|
|
}
|
|
:deep(.map-tile-text) {
|
|
position: absolute;
|
|
left: 2px;
|
|
top: 2px;
|
|
}
|
|
</style>
|