Update project documentation and improve frontend functionality

- Updated the backend documentation in CONTRIBUTING.md and README.md to reflect changes in application structure and API endpoints.
- Enhanced the frontend components in MapView.vue for better handling of context menu actions.
- Added new types and interfaces in TypeScript for improved type safety in the frontend.
- Introduced new utility classes for managing characters and markers in the map.
- Updated .gitignore to include .vscode directory for better development environment management.
This commit is contained in:
2026-02-24 23:32:50 +03:00
parent 605a31567e
commit 82cb8a13f5
39 changed files with 1788 additions and 2631 deletions

View File

@@ -1,24 +1,46 @@
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
import * as L from 'leaflet'
export interface CharacterData {
name: string
position: { x: number; y: number }
type: string
id: number
map: number
}
export interface MapViewRef {
map: L.Map
mapid: number
markerLayer?: L.LayerGroup
}
export class Character {
constructor(characterData) {
name: string
position: { x: number; y: number }
type: string
id: number
map: number
marker: L.Marker | null = null
text: string
value: number
onClick: ((e: L.LeafletMouseEvent) => void) | null = null
constructor(characterData: CharacterData) {
this.name = characterData.name
this.position = characterData.position
this.type = characterData.type
this.id = characterData.id
this.map = characterData.map
this.marker = null
this.text = this.name
this.value = this.id
this.onClick = null
}
getId() {
getId(): string {
return `${this.name}`
}
remove(mapview) {
remove(mapview: MapViewRef): void {
if (this.marker) {
const layer = mapview.markerLayer ?? mapview.map
layer.removeLayer(this.marker)
@@ -26,7 +48,7 @@ export class Character {
}
}
add(mapview) {
add(mapview: MapViewRef): void {
if (this.map === mapview.mapid) {
const position = mapview.map.unproject([this.position.x, this.position.y], HnHMaxZoom)
this.marker = L.marker(position, { title: this.name })
@@ -36,7 +58,7 @@ export class Character {
}
}
update(mapview, updated) {
update(mapview: MapViewRef, updated: CharacterData): void {
if (this.map !== updated.map) {
this.remove(mapview)
}
@@ -51,11 +73,11 @@ export class Character {
}
}
setClickCallback(callback) {
setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void {
this.onClick = callback
}
callCallback(e) {
callCallback(e: L.LeafletMouseEvent): void {
if (this.onClick != null) this.onClick(e)
}
}

View File

@@ -12,7 +12,7 @@ export const GridCoordLayer = L.GridLayer.extend({
options: {
visible: true,
},
createTile(coords) {
createTile(coords: { x: number; y: number; z: number }) {
if (!this.options.visible) {
const element = document.createElement('div')
element.style.width = TileSize + 'px'
@@ -61,23 +61,23 @@ export const GridCoordLayer = L.GridLayer.extend({
}
return element
},
})
}) as unknown as new (options?: L.GridLayerOptions) => L.GridLayer
export const ImageIcon = L.Icon.extend({
options: {
iconSize: [32, 32],
iconAnchor: [16, 16],
},
})
}) as unknown as new (options?: L.IconOptions) => L.Icon
const latNormalization = (90.0 * TileSize) / 2500000.0
const lngNormalization = (180.0 * TileSize) / 2500000.0
const HnHProjection = {
project(latlng) {
project(latlng: LatLng) {
return new Point(latlng.lat / latNormalization, latlng.lng / lngNormalization)
},
unproject(point) {
unproject(point: Point) {
return new LatLng(point.x * latNormalization, point.y * lngNormalization)
},
bounds: (() => new Bounds([-latNormalization, -lngNormalization], [latNormalization, lngNormalization]))(),
@@ -85,4 +85,4 @@ const HnHProjection = {
export const HnHCRS = L.extend({}, L.CRS.Simple, {
projection: HnHProjection,
})
}) as L.CRS

View File

@@ -1,38 +1,63 @@
import { HnHMaxZoom, ImageIcon } from '~/lib/LeafletCustomTypes'
import * as L from 'leaflet'
function detectType(name) {
export interface MarkerData {
id: number
position: { x: number; y: number }
name: string
image: string
hidden: boolean
map: number
}
export interface MapViewRef {
map: L.Map
mapid: number
markerLayer: L.LayerGroup
}
function detectType(name: string): string {
if (name === 'gfx/invobjs/small/bush' || name === 'gfx/invobjs/small/bumling') return 'quest'
if (name === 'custom') return 'custom'
return name.substring('gfx/terobjs/mm/'.length)
}
export class Marker {
constructor(markerData) {
id: number
position: { x: number; y: number }
name: string
image: string
type: string
marker: L.Marker | null = null
text: string
value: number
hidden: boolean
map: number
onClick: ((e: L.LeafletMouseEvent) => void) | null = null
onContext: ((e: L.LeafletMouseEvent) => void) | null = null
constructor(markerData: MarkerData) {
this.id = markerData.id
this.position = markerData.position
this.name = markerData.name
this.image = markerData.image
this.type = detectType(this.image)
this.marker = null
this.text = this.name
this.value = this.id
this.hidden = markerData.hidden
this.map = markerData.map
this.onClick = null
this.onContext = null
}
remove(mapview) {
remove(_mapview: MapViewRef): void {
if (this.marker) {
this.marker.remove()
this.marker = null
}
}
add(mapview) {
add(mapview: MapViewRef): void {
if (!this.hidden) {
let icon
let icon: L.Icon
if (this.image === 'gfx/terobjs/mm/custom') {
icon = new ImageIcon({
iconUrl: 'gfx/terobjs/mm/custom.png',
@@ -53,7 +78,7 @@ export class Marker {
}
}
update(mapview, updated) {
update(mapview: MapViewRef, updated: MarkerData): void {
this.position = updated.position
this.name = updated.name
this.hidden = updated.hidden
@@ -64,26 +89,26 @@ export class Marker {
}
}
jumpTo(map) {
jumpTo(map: L.Map): void {
if (this.marker) {
const position = map.unproject([this.position.x, this.position.y], HnHMaxZoom)
this.marker.setLatLng(position)
}
}
setClickCallback(callback) {
setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void {
this.onClick = callback
}
callClickCallback(e) {
callClickCallback(e: L.LeafletMouseEvent): void {
if (this.onClick != null) this.onClick(e)
}
setContextMenu(callback) {
setContextMenu(callback: (e: L.LeafletMouseEvent) => void): void {
this.onContext = callback
}
callContextCallback(e) {
callContextCallback(e: L.LeafletMouseEvent): void {
if (this.onContext != null) this.onContext(e)
}
}

View File

@@ -1,11 +1,15 @@
import L, { Util, Browser } from 'leaflet'
interface SmartTileLayerCache {
[key: string]: number | undefined
}
export const SmartTileLayer = L.TileLayer.extend({
cache: {},
cache: {} as SmartTileLayerCache,
invalidTile: '',
map: 0,
getTileUrl(coords) {
getTileUrl(coords: { x: number; y: number; z: number }) {
if (!this._map) return this.invalidTile
let zoom
try {
@@ -16,8 +20,8 @@ export const SmartTileLayer = L.TileLayer.extend({
return this.getTrueTileUrl(coords, zoom)
},
getTrueTileUrl(coords, zoom) {
const data = {
getTrueTileUrl(coords: { x: number; y: number }, zoom: number) {
const data: Record<string, string | number | undefined> = {
r: Browser.retina ? '@2x' : '',
s: this._getSubdomain(coords),
x: coords.x,
@@ -37,7 +41,8 @@ export const SmartTileLayer = L.TileLayer.extend({
data.cache = this.cache[cacheKey]
// Don't request tiles for invalid/unknown map (avoids 404 spam in console)
if (data.map === undefined || data.map === null || data.map < 1) {
const mapId = Number(data.map)
if (data.map === undefined || data.map === null || mapId < 1) {
return this.invalidTile
}
// Only use placeholder when server explicitly marks tile as invalid (-1)
@@ -53,7 +58,7 @@ export const SmartTileLayer = L.TileLayer.extend({
return Util.template(this._url, Util.extend(data, this.options))
},
refresh(x, y, z) {
refresh(x: number, y: number, z: number) {
let zoom = z
const maxZoom = this.options.maxZoom
const zoomReverse = this.options.zoomReverse
@@ -70,4 +75,10 @@ export const SmartTileLayer = L.TileLayer.extend({
tile.el.src = this.getTrueTileUrl({ x, y }, z)
}
},
})
}) as unknown as new (urlTemplate: string, options?: L.TileLayerOptions) => L.TileLayer & {
cache: SmartTileLayerCache
invalidTile: string
map: number
getTrueTileUrl: (coords: { x: number; y: number }, zoom: number) => string
refresh: (x: number, y: number, z: number) => void
}

View File

@@ -1,39 +0,0 @@
/**
* Elements should have unique field "id"
*/
export class UniqueList {
constructor() {
this.elements = {}
}
update(dataList, addCallback, removeCallback, updateCallback) {
const elementsToAdd = dataList.filter((it) => this.elements[it.id] === undefined)
const elementsToRemove = Object.keys(this.elements)
.filter((it) => dataList.find((up) => String(up.id) === it) === undefined)
.map((id) => this.elements[id])
if (removeCallback) {
elementsToRemove.forEach((it) => removeCallback(it))
}
if (updateCallback) {
dataList.forEach((newElement) => {
const oldElement = this.elements[newElement.id]
if (oldElement) {
updateCallback(oldElement, newElement)
}
})
}
if (addCallback) {
elementsToAdd.forEach((it) => addCallback(it))
}
elementsToRemove.forEach((it) => delete this.elements[it.id])
elementsToAdd.forEach((it) => (this.elements[it.id] = it))
}
getElements() {
return Object.values(this.elements)
}
byId(id) {
return this.elements[id]
}
}

View File

@@ -0,0 +1,50 @@
/**
* Elements should have unique field "id"
*/
export interface Identifiable {
id: number | string
}
export class UniqueList<T extends Identifiable> {
elements: Record<string, T> = {}
update(
dataList: T[],
addCallback?: (it: T) => void,
removeCallback?: (it: T) => void,
updateCallback?: (oldElement: T, newElement: T) => void
): void {
const elementsToAdd = dataList.filter((it) => this.elements[String(it.id)] === undefined)
const elementsToRemove: T[] = []
for (const id of Object.keys(this.elements)) {
if (dataList.find((up) => String(up.id) === id) === undefined) {
const el = this.elements[id]
if (el) elementsToRemove.push(el)
}
}
if (removeCallback) {
elementsToRemove.forEach((it) => removeCallback(it))
}
if (updateCallback) {
dataList.forEach((newElement) => {
const oldElement = this.elements[String(newElement.id)]
if (oldElement) {
updateCallback(oldElement, newElement)
}
})
}
if (addCallback) {
elementsToAdd.forEach((it) => addCallback(it))
}
elementsToRemove.forEach((it) => delete this.elements[String(it.id)])
elementsToAdd.forEach((it) => (this.elements[String(it.id)] = it))
}
getElements(): T[] {
return Object.values(this.elements)
}
byId(id: number | string): T | undefined {
return this.elements[String(id)]
}
}