26-1-2026
This commit is contained in:
@@ -49,6 +49,67 @@ struct Position {
|
||||
y: f32,
|
||||
}
|
||||
|
||||
// Labels response (world_map_icons with icon_type == 16)
|
||||
#[derive(Serialize)]
|
||||
struct LabelsResponse {
|
||||
labels: Vec<Label>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Label {
|
||||
x: f32,
|
||||
y: f32,
|
||||
text: String,
|
||||
font_size: i32,
|
||||
}
|
||||
|
||||
// Entrances response (world_teleporters)
|
||||
#[derive(Serialize)]
|
||||
struct EntrancesResponse {
|
||||
icon_base64: String,
|
||||
entrances: Vec<Entrance>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Entrance {
|
||||
pos_x: f32,
|
||||
pos_y: f32,
|
||||
tp_x: Option<f32>,
|
||||
tp_y: Option<f32>,
|
||||
}
|
||||
|
||||
// Ground Items response (world_loot)
|
||||
#[derive(Serialize)]
|
||||
struct GroundItemsResponse {
|
||||
icon_base64: String,
|
||||
items: Vec<GroundItem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GroundItem {
|
||||
x: f32,
|
||||
y: f32,
|
||||
name: String,
|
||||
amount: i32,
|
||||
respawn_time: i32,
|
||||
}
|
||||
|
||||
// Houses response (player_houses)
|
||||
#[derive(Serialize)]
|
||||
struct HousesResponse {
|
||||
icon_base64: String,
|
||||
houses: Vec<House>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct House {
|
||||
x: f32,
|
||||
y: f32,
|
||||
name: String,
|
||||
description: String,
|
||||
price: i32,
|
||||
}
|
||||
|
||||
// Establish database connection
|
||||
fn establish_connection(database_url: &str) -> Result<DbConnection, diesel::ConnectionError> {
|
||||
SqliteConnection::establish(database_url)
|
||||
@@ -201,6 +262,212 @@ async fn get_resources(
|
||||
Ok(Json(ResourceResponse { resources }))
|
||||
}
|
||||
|
||||
// Get labels from world_map_icons where icon_type == 16
|
||||
async fn get_labels(State(state): State<Arc<AppState>>) -> Result<Json<LabelsResponse>, StatusCode> {
|
||||
use cursebreaker_parser::schema::world_map_icons;
|
||||
|
||||
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||
tracing::error!("Database connection error: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let results = world_map_icons::table
|
||||
.filter(world_map_icons::icon_type.eq(16))
|
||||
.select((
|
||||
world_map_icons::pos_x,
|
||||
world_map_icons::pos_y,
|
||||
world_map_icons::text,
|
||||
world_map_icons::font_size,
|
||||
))
|
||||
.load::<(f32, f32, String, i32)>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying labels: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let labels: Vec<Label> = results
|
||||
.into_iter()
|
||||
.map(|(pos_x, pos_y, text, font_size)| Label {
|
||||
x: pos_x * 5.12,
|
||||
y: pos_y * 5.12,
|
||||
text,
|
||||
font_size,
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Returning {} labels", labels.len());
|
||||
|
||||
Ok(Json(LabelsResponse { labels }))
|
||||
}
|
||||
|
||||
// Get entrances from world_teleporters
|
||||
async fn get_entrances(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<EntrancesResponse>, StatusCode> {
|
||||
use cursebreaker_parser::schema::{general_icons, world_teleporters};
|
||||
|
||||
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||
tracing::error!("Database connection error: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
// Get the Entrance icon
|
||||
let icon_bytes: Vec<u8> = general_icons::table
|
||||
.filter(general_icons::name.eq("Entrance"))
|
||||
.select(general_icons::icon_32)
|
||||
.first::<Option<Vec<u8>>>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying entrance icon: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
.unwrap_or_default();
|
||||
|
||||
let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes);
|
||||
|
||||
// Get teleporter positions
|
||||
let results = world_teleporters::table
|
||||
.select((
|
||||
world_teleporters::pos_x,
|
||||
world_teleporters::pos_y,
|
||||
world_teleporters::tp_x,
|
||||
world_teleporters::tp_y,
|
||||
))
|
||||
.load::<(f32, f32, Option<f32>, Option<f32>)>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying teleporters: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let entrances: Vec<Entrance> = results
|
||||
.into_iter()
|
||||
.map(|(pos_x, pos_y, tp_x, tp_y)| Entrance {
|
||||
pos_x: pos_x * 5.12,
|
||||
pos_y: pos_y * 5.12,
|
||||
tp_x: tp_x.map(|x| x * 5.12),
|
||||
tp_y: tp_y.map(|y| y * 5.12),
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Returning {} entrances", entrances.len());
|
||||
|
||||
Ok(Json(EntrancesResponse {
|
||||
icon_base64,
|
||||
entrances,
|
||||
}))
|
||||
}
|
||||
|
||||
// Get ground items from world_loot
|
||||
async fn get_ground_items(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<GroundItemsResponse>, StatusCode> {
|
||||
use cursebreaker_parser::schema::{general_icons, items, world_loot};
|
||||
|
||||
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||
tracing::error!("Database connection error: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
// Get the Common_tick icon
|
||||
let icon_bytes: Vec<u8> = general_icons::table
|
||||
.filter(general_icons::name.eq("Common_tick"))
|
||||
.select(general_icons::icon_32)
|
||||
.first::<Option<Vec<u8>>>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying common_tick icon: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
.unwrap_or_default();
|
||||
|
||||
let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes);
|
||||
|
||||
// Get world loot with item names
|
||||
let results = world_loot::table
|
||||
.inner_join(items::table.on(world_loot::item_id.eq(items::id.assume_not_null())))
|
||||
.select((
|
||||
world_loot::pos_x,
|
||||
world_loot::pos_y,
|
||||
items::name,
|
||||
world_loot::amount,
|
||||
world_loot::respawn_time,
|
||||
))
|
||||
.load::<(f32, f32, String, i32, i32)>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying ground items: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let ground_items: Vec<GroundItem> = results
|
||||
.into_iter()
|
||||
.map(|(pos_x, pos_y, name, amount, respawn_time)| GroundItem {
|
||||
x: pos_x * 5.12,
|
||||
y: pos_y * 5.12,
|
||||
name,
|
||||
amount,
|
||||
respawn_time,
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Returning {} ground items", ground_items.len());
|
||||
|
||||
Ok(Json(GroundItemsResponse {
|
||||
icon_base64,
|
||||
items: ground_items,
|
||||
}))
|
||||
}
|
||||
|
||||
// Get player houses
|
||||
async fn get_houses(State(state): State<Arc<AppState>>) -> Result<Json<HousesResponse>, StatusCode> {
|
||||
use cursebreaker_parser::schema::{general_icons, player_houses};
|
||||
|
||||
let mut conn = establish_connection(&state.database_url).map_err(|e| {
|
||||
tracing::error!("Database connection error: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
// Get the Notifications_House icon (64px)
|
||||
let icon_bytes: Vec<u8> = general_icons::table
|
||||
.filter(general_icons::name.eq("Notifications_House"))
|
||||
.select(general_icons::icon_64)
|
||||
.first::<Option<Vec<u8>>>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying house icon: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?
|
||||
.unwrap_or_default();
|
||||
|
||||
let icon_base64 = base64::engine::general_purpose::STANDARD.encode(&icon_bytes);
|
||||
|
||||
// Get player houses
|
||||
let results = player_houses::table
|
||||
.select((
|
||||
player_houses::pos_x,
|
||||
player_houses::pos_z,
|
||||
player_houses::name,
|
||||
player_houses::description,
|
||||
player_houses::price,
|
||||
))
|
||||
.load::<(f32, f32, String, String, i32)>(&mut conn)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error querying player houses: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let houses: Vec<House> = results
|
||||
.into_iter()
|
||||
.map(|(pos_x, pos_z, name, description, price)| House {
|
||||
x: pos_x * 5.12,
|
||||
y: pos_z * 5.12,
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Returning {} houses", houses.len());
|
||||
|
||||
Ok(Json(HousesResponse { icon_base64, houses }))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize tracing
|
||||
@@ -219,6 +486,10 @@ async fn main() {
|
||||
.route("/api/bounds", get(get_bounds))
|
||||
.route("/api/tiles/:z/:x/:y", get(get_tile))
|
||||
.route("/api/resources", get(get_resources))
|
||||
.route("/api/labels", get(get_labels))
|
||||
.route("/api/entrances", get(get_entrances))
|
||||
.route("/api/ground-items", get(get_ground_items))
|
||||
.route("/api/houses", get(get_houses))
|
||||
.nest_service("/", ServeDir::new("static"))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
@@ -21,6 +21,46 @@
|
||||
<p class="subtitle">The Black Grimoire: Cursebreaker</p>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<h3>Labels</h3>
|
||||
<div class="filter-controls">
|
||||
<label class="filter-label master-toggle">
|
||||
<input type="checkbox" id="labels-toggle" checked>
|
||||
<span>Show Labels</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<h3>Entrances</h3>
|
||||
<div class="filter-controls">
|
||||
<label class="filter-label master-toggle">
|
||||
<input type="checkbox" id="entrances-toggle" checked>
|
||||
<span>Show Entrances</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<h3>Ground Items</h3>
|
||||
<div class="filter-controls">
|
||||
<label class="filter-label master-toggle">
|
||||
<input type="checkbox" id="ground-items-toggle" checked>
|
||||
<span>Show Ground Items</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<h3>Houses</h3>
|
||||
<div class="filter-controls">
|
||||
<label class="filter-label master-toggle">
|
||||
<input type="checkbox" id="houses-toggle" checked>
|
||||
<span>Show Houses</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<h3>Resources</h3>
|
||||
<div class="filter-controls">
|
||||
@@ -59,5 +99,6 @@
|
||||
<!-- Custom JS -->
|
||||
<script src="map.js"></script>
|
||||
<script src="resources.js"></script>
|
||||
<script src="markers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -120,6 +120,9 @@ async function initMap() {
|
||||
console.error('Failed to load resources:', error);
|
||||
});
|
||||
|
||||
// Load markers (labels, entrances, ground items, houses)
|
||||
initMarkers();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing map:', error);
|
||||
document.getElementById('map-stats').innerHTML =
|
||||
|
||||
377
cursebreaker-map/static/markers.js
Normal file
377
cursebreaker-map/static/markers.js
Normal file
@@ -0,0 +1,377 @@
|
||||
// Markers management for Cursebreaker map (Labels, Entrances, Ground Items, Houses)
|
||||
|
||||
// Layer groups for each marker type
|
||||
let labelsLayerGroup = null;
|
||||
let entrancesLayerGroup = null;
|
||||
let groundItemsLayerGroup = null;
|
||||
let housesLayerGroup = null;
|
||||
|
||||
// Store active teleport lines for entrances
|
||||
let activeTeleportLine = null;
|
||||
|
||||
// Initialize all markers when map is ready
|
||||
function initMarkers() {
|
||||
// Load all marker types in parallel
|
||||
Promise.all([
|
||||
loadLabels(),
|
||||
loadEntrances(),
|
||||
loadGroundItems(),
|
||||
loadHouses(),
|
||||
]).catch(error => {
|
||||
console.error('Error loading markers:', error);
|
||||
});
|
||||
|
||||
// Set up toggle handlers
|
||||
setupMarkerToggles();
|
||||
}
|
||||
|
||||
// Set up toggle event handlers
|
||||
function setupMarkerToggles() {
|
||||
const labelsToggle = document.getElementById('labels-toggle');
|
||||
const entrancesToggle = document.getElementById('entrances-toggle');
|
||||
const groundItemsToggle = document.getElementById('ground-items-toggle');
|
||||
const housesToggle = document.getElementById('houses-toggle');
|
||||
|
||||
if (labelsToggle) {
|
||||
labelsToggle.addEventListener('change', (e) => {
|
||||
toggleLayer(labelsLayerGroup, e.target.checked);
|
||||
saveMarkerState('labels', e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
if (entrancesToggle) {
|
||||
entrancesToggle.addEventListener('change', (e) => {
|
||||
toggleLayer(entrancesLayerGroup, e.target.checked);
|
||||
saveMarkerState('entrances', e.target.checked);
|
||||
// Remove active teleport line when hiding entrances
|
||||
if (!e.target.checked && activeTeleportLine) {
|
||||
map.removeLayer(activeTeleportLine);
|
||||
activeTeleportLine = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (groundItemsToggle) {
|
||||
groundItemsToggle.addEventListener('change', (e) => {
|
||||
toggleLayer(groundItemsLayerGroup, e.target.checked);
|
||||
saveMarkerState('groundItems', e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
if (housesToggle) {
|
||||
housesToggle.addEventListener('change', (e) => {
|
||||
toggleLayer(housesLayerGroup, e.target.checked);
|
||||
saveMarkerState('houses', e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// Restore saved state
|
||||
restoreMarkerState();
|
||||
}
|
||||
|
||||
// Toggle layer visibility
|
||||
function toggleLayer(layerGroup, visible) {
|
||||
if (!layerGroup) return;
|
||||
|
||||
if (visible) {
|
||||
layerGroup.addTo(map);
|
||||
} else {
|
||||
map.removeLayer(layerGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Save marker visibility state
|
||||
function saveMarkerState(type, visible) {
|
||||
try {
|
||||
const state = JSON.parse(localStorage.getItem('cursebreaker_marker_state') || '{}');
|
||||
state[type] = visible;
|
||||
localStorage.setItem('cursebreaker_marker_state', JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save marker state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore marker visibility state
|
||||
function restoreMarkerState() {
|
||||
try {
|
||||
const state = JSON.parse(localStorage.getItem('cursebreaker_marker_state') || '{}');
|
||||
|
||||
// Update checkboxes and layers based on saved state
|
||||
setTimeout(() => {
|
||||
if (state.labels === false) {
|
||||
const toggle = document.getElementById('labels-toggle');
|
||||
if (toggle) {
|
||||
toggle.checked = false;
|
||||
toggleLayer(labelsLayerGroup, false);
|
||||
}
|
||||
}
|
||||
if (state.entrances === false) {
|
||||
const toggle = document.getElementById('entrances-toggle');
|
||||
if (toggle) {
|
||||
toggle.checked = false;
|
||||
toggleLayer(entrancesLayerGroup, false);
|
||||
}
|
||||
}
|
||||
if (state.groundItems === false) {
|
||||
const toggle = document.getElementById('ground-items-toggle');
|
||||
if (toggle) {
|
||||
toggle.checked = false;
|
||||
toggleLayer(groundItemsLayerGroup, false);
|
||||
}
|
||||
}
|
||||
if (state.houses === false) {
|
||||
const toggle = document.getElementById('houses-toggle');
|
||||
if (toggle) {
|
||||
toggle.checked = false;
|
||||
toggleLayer(housesLayerGroup, false);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore marker state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load labels (text markers on the map)
|
||||
async function loadLabels() {
|
||||
try {
|
||||
console.log('Loading labels...');
|
||||
const response = await fetch('/api/labels');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Received ${data.labels.length} labels`);
|
||||
|
||||
labelsLayerGroup = L.layerGroup();
|
||||
|
||||
for (const label of data.labels) {
|
||||
// Create a divIcon with the label text
|
||||
const labelIcon = L.divIcon({
|
||||
className: 'map-label',
|
||||
html: `<div class="label-text" style="font-size: ${label.font_size}px;">${label.text}</div>`,
|
||||
iconSize: null, // Let CSS handle sizing
|
||||
iconAnchor: [0, 0],
|
||||
});
|
||||
|
||||
const marker = L.marker([label.y, label.x], {
|
||||
icon: labelIcon,
|
||||
interactive: false, // Labels shouldn't be clickable
|
||||
});
|
||||
|
||||
marker.addTo(labelsLayerGroup);
|
||||
}
|
||||
|
||||
labelsLayerGroup.addTo(map);
|
||||
console.log('Labels loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Error loading labels:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load entrances (teleporters with lines)
|
||||
async function loadEntrances() {
|
||||
try {
|
||||
console.log('Loading entrances...');
|
||||
const response = await fetch('/api/entrances');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Received ${data.entrances.length} entrances`);
|
||||
|
||||
entrancesLayerGroup = L.layerGroup();
|
||||
|
||||
// Create icon from base64
|
||||
const iconUrl = `data:image/webp;base64,${data.icon_base64}`;
|
||||
const entranceIcon = L.icon({
|
||||
iconUrl: iconUrl,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
popupAnchor: [0, -16],
|
||||
});
|
||||
|
||||
for (const entrance of data.entrances) {
|
||||
const marker = L.marker([entrance.pos_y, entrance.pos_x], {
|
||||
icon: entranceIcon,
|
||||
title: 'Entrance',
|
||||
});
|
||||
|
||||
// Store teleport destination on the marker
|
||||
marker.teleportDest = {
|
||||
x: entrance.tp_x,
|
||||
y: entrance.tp_y,
|
||||
};
|
||||
|
||||
// Handle click to show teleport line
|
||||
marker.on('click', function(e) {
|
||||
// Remove existing line if any
|
||||
if (activeTeleportLine) {
|
||||
map.removeLayer(activeTeleportLine);
|
||||
activeTeleportLine = null;
|
||||
}
|
||||
|
||||
const dest = this.teleportDest;
|
||||
if (dest.x !== null && dest.y !== null) {
|
||||
// Create a line from entrance to destination
|
||||
activeTeleportLine = L.polyline(
|
||||
[
|
||||
[entrance.pos_y, entrance.pos_x],
|
||||
[dest.y, dest.x]
|
||||
],
|
||||
{
|
||||
color: '#00ffff',
|
||||
weight: 3,
|
||||
opacity: 0.8,
|
||||
dashArray: '10, 10',
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
// Add a destination marker
|
||||
const destMarker = L.circleMarker([dest.y, dest.x], {
|
||||
radius: 8,
|
||||
color: '#00ffff',
|
||||
fillColor: '#00ffff',
|
||||
fillOpacity: 0.5,
|
||||
}).addTo(map);
|
||||
|
||||
// Remove line and destination marker after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (activeTeleportLine) {
|
||||
map.removeLayer(activeTeleportLine);
|
||||
activeTeleportLine = null;
|
||||
}
|
||||
map.removeLayer(destMarker);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
marker.addTo(entrancesLayerGroup);
|
||||
}
|
||||
|
||||
entrancesLayerGroup.addTo(map);
|
||||
console.log('Entrances loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Error loading entrances:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Format respawn time as "XXM XXS"
|
||||
function formatRespawnTime(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (minutes > 0 && secs > 0) {
|
||||
return `${minutes}M ${secs}S`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}M`;
|
||||
} else {
|
||||
return `${secs}S`;
|
||||
}
|
||||
}
|
||||
|
||||
// Load ground items
|
||||
async function loadGroundItems() {
|
||||
try {
|
||||
console.log('Loading ground items...');
|
||||
const response = await fetch('/api/ground-items');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Received ${data.items.length} ground items`);
|
||||
|
||||
groundItemsLayerGroup = L.layerGroup();
|
||||
|
||||
// Create icon from base64
|
||||
const iconUrl = `data:image/webp;base64,${data.icon_base64}`;
|
||||
const itemIcon = L.icon({
|
||||
iconUrl: iconUrl,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12],
|
||||
popupAnchor: [0, -12],
|
||||
});
|
||||
|
||||
for (const item of data.items) {
|
||||
const marker = L.marker([item.y, item.x], {
|
||||
icon: itemIcon,
|
||||
title: item.name,
|
||||
});
|
||||
|
||||
// Build popup content
|
||||
let popupContent = `<strong>${item.name}</strong>`;
|
||||
if (item.amount > 1) {
|
||||
popupContent += `<br/>Amount: ${item.amount}`;
|
||||
}
|
||||
popupContent += `<br/>Respawn: ${formatRespawnTime(item.respawn_time)}`;
|
||||
|
||||
marker.bindPopup(popupContent);
|
||||
marker.addTo(groundItemsLayerGroup);
|
||||
}
|
||||
|
||||
groundItemsLayerGroup.addTo(map);
|
||||
console.log('Ground items loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Error loading ground items:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load player houses
|
||||
async function loadHouses() {
|
||||
try {
|
||||
console.log('Loading houses...');
|
||||
const response = await fetch('/api/houses');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Received ${data.houses.length} houses`);
|
||||
|
||||
housesLayerGroup = L.layerGroup();
|
||||
|
||||
// Create icon from base64
|
||||
const iconUrl = `data:image/webp;base64,${data.icon_base64}`;
|
||||
const houseIcon = L.icon({
|
||||
iconUrl: iconUrl,
|
||||
iconSize: [64, 64],
|
||||
iconAnchor: [32, 32],
|
||||
popupAnchor: [0, -32],
|
||||
});
|
||||
|
||||
for (const house of data.houses) {
|
||||
const marker = L.marker([house.y, house.x], {
|
||||
icon: houseIcon,
|
||||
title: house.name,
|
||||
});
|
||||
|
||||
// Format price with commas
|
||||
const formattedPrice = house.price.toLocaleString();
|
||||
|
||||
// Build popup content
|
||||
const popupContent = `
|
||||
<strong>${house.name}</strong><br/>
|
||||
<em>${house.description}</em><br/>
|
||||
<span class="house-price">Price: ${formattedPrice} gold</span>
|
||||
`;
|
||||
|
||||
marker.bindPopup(popupContent);
|
||||
marker.addTo(housesLayerGroup);
|
||||
}
|
||||
|
||||
housesLayerGroup.addTo(map);
|
||||
console.log('Houses loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Error loading houses:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Call initMarkers after map is loaded
|
||||
// This is called from map.js after resources are loaded
|
||||
@@ -259,6 +259,53 @@ body {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
/* Master toggle for marker categories */
|
||||
.master-toggle {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.master-toggle input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Map labels (text overlays) */
|
||||
.map-label {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
color: #e0e0e0;
|
||||
text-shadow:
|
||||
-1px -1px 2px #000,
|
||||
1px -1px 2px #000,
|
||||
-1px 1px 2px #000,
|
||||
1px 1px 2px #000,
|
||||
0 0 4px #000;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* House price styling in popup */
|
||||
.house-price {
|
||||
color: #ffd700;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Popup styling for various marker types */
|
||||
.leaflet-popup-content strong {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.leaflet-popup-content em {
|
||||
color: #a0a0a0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
|
||||
Reference in New Issue
Block a user