471 lines
14 KiB
JavaScript
471 lines
14 KiB
JavaScript
// Map Editor JavaScript
|
|
let currentLocations = [];
|
|
let availableNPCs = [];
|
|
let selectedLocationId = null;
|
|
let canvas, ctx;
|
|
let scale = 50;
|
|
let offsetX = 0;
|
|
let offsetY = 0;
|
|
|
|
// Check authentication on load
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
const response = await fetch('/api/check-auth');
|
|
const data = await response.json();
|
|
|
|
if (data.authenticated) {
|
|
showEditor();
|
|
} else {
|
|
document.getElementById('loginContainer').style.display = 'flex';
|
|
}
|
|
});
|
|
|
|
// Login form handler
|
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const password = document.getElementById('password').value;
|
|
|
|
try {
|
|
const response = await fetch('/api/login', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({password})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showEditor();
|
|
} else {
|
|
showError(data.message);
|
|
}
|
|
} catch (error) {
|
|
showError('Login failed: ' + error.message);
|
|
}
|
|
});
|
|
|
|
function showError(message) {
|
|
const errorDiv = document.getElementById('loginError');
|
|
errorDiv.textContent = message;
|
|
errorDiv.style.display = 'block';
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
const successDiv = document.getElementById('saveSuccess');
|
|
successDiv.textContent = message;
|
|
successDiv.style.display = 'block';
|
|
setTimeout(() => {
|
|
successDiv.style.display = 'none';
|
|
}, 3000);
|
|
}
|
|
|
|
async function showEditor() {
|
|
document.getElementById('loginContainer').style.display = 'none';
|
|
document.getElementById('editorContainer').style.display = 'grid';
|
|
|
|
// Initialize canvas
|
|
canvas = document.getElementById('editorCanvas');
|
|
ctx = canvas.getContext('2d');
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
// Load data
|
|
await loadLocations();
|
|
await loadAvailableNPCs();
|
|
|
|
// Draw map
|
|
drawMap();
|
|
|
|
// Canvas click handler
|
|
canvas.addEventListener('click', handleCanvasClick);
|
|
}
|
|
|
|
function resizeCanvas() {
|
|
canvas.width = canvas.offsetWidth;
|
|
canvas.height = canvas.offsetHeight;
|
|
drawMap();
|
|
}
|
|
|
|
async function loadLocations() {
|
|
try {
|
|
const response = await fetch('/api/editor/locations');
|
|
const data = await response.json();
|
|
currentLocations = data.locations;
|
|
renderLocationList();
|
|
} catch (error) {
|
|
console.error('Failed to load locations:', error);
|
|
}
|
|
}
|
|
|
|
async function loadAvailableNPCs() {
|
|
try {
|
|
const response = await fetch('/api/editor/available-npcs');
|
|
const data = await response.json();
|
|
availableNPCs = data.npcs;
|
|
} catch (error) {
|
|
console.error('Failed to load NPCs:', error);
|
|
}
|
|
}
|
|
|
|
function renderLocationList() {
|
|
const list = document.getElementById('locationList');
|
|
list.innerHTML = '';
|
|
|
|
currentLocations.forEach(location => {
|
|
const item = document.createElement('div');
|
|
item.className = 'location-item';
|
|
if (location.id === selectedLocationId) {
|
|
item.classList.add('active');
|
|
}
|
|
|
|
item.innerHTML = `
|
|
<div class="location-item-name">${location.name}</div>
|
|
<div class="location-item-coords">📍 (${location.x}, ${location.y}) | Danger: ${location.danger_level}</div>
|
|
`;
|
|
|
|
item.onclick = () => selectLocation(location.id);
|
|
list.appendChild(item);
|
|
});
|
|
}
|
|
|
|
async function selectLocation(locationId) {
|
|
selectedLocationId = locationId;
|
|
renderLocationList();
|
|
|
|
// Load full location details
|
|
try {
|
|
const response = await fetch(`/api/editor/location/${locationId}`);
|
|
const location = await response.json();
|
|
populateForm(location);
|
|
} catch (error) {
|
|
console.error('Failed to load location details:', error);
|
|
}
|
|
|
|
drawMap();
|
|
}
|
|
|
|
function populateForm(location) {
|
|
document.getElementById('noSelectionMessage').classList.add('hidden');
|
|
document.getElementById('propertiesForm').classList.remove('hidden');
|
|
|
|
document.getElementById('locationId').value = location.id;
|
|
document.getElementById('locationName').value = location.name;
|
|
document.getElementById('locationDescription').value = location.description;
|
|
document.getElementById('locationX').value = location.x;
|
|
document.getElementById('locationY').value = location.y;
|
|
document.getElementById('dangerLevel').value = location.danger_level;
|
|
document.getElementById('encounterRate').value = location.encounter_rate;
|
|
document.getElementById('wanderingChance').value = location.wandering_chance;
|
|
document.getElementById('imagePath').value = location.image_path || '';
|
|
|
|
// Update image preview
|
|
updateImagePreview(location.image_path);
|
|
|
|
// Render spawn list
|
|
renderSpawnList(location.spawn_npcs);
|
|
}
|
|
|
|
function updateImagePreview(imagePath) {
|
|
const preview = document.getElementById('imagePreview');
|
|
if (imagePath) {
|
|
preview.innerHTML = `<img src="/${imagePath}" alt="Location image">`;
|
|
} else {
|
|
preview.innerHTML = '<span>No image</span>';
|
|
}
|
|
}
|
|
|
|
function renderSpawnList(spawns) {
|
|
const list = document.getElementById('spawnList');
|
|
list.innerHTML = '';
|
|
|
|
spawns.forEach((spawn, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'spawn-item';
|
|
item.innerHTML = `
|
|
<div class="spawn-item-info">
|
|
<div class="spawn-item-name">${spawn.emoji} ${spawn.name}</div>
|
|
<div class="spawn-item-weight">Weight: ${spawn.weight}</div>
|
|
</div>
|
|
<button class="btn btn-remove" onclick="removeSpawn(${index})">Remove</button>
|
|
`;
|
|
list.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function drawMap() {
|
|
ctx.fillStyle = '#0f0f1e';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw grid
|
|
ctx.strokeStyle = '#1a1a3e';
|
|
ctx.lineWidth = 1;
|
|
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
|
|
// Draw vertical lines
|
|
for (let x = centerX % scale; x < canvas.width; x += scale) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, 0);
|
|
ctx.lineTo(x, canvas.height);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Draw horizontal lines
|
|
for (let y = centerY % scale; y < canvas.height; y += scale) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(canvas.width, y);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Draw axes
|
|
ctx.strokeStyle = '#3a3a6a';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(centerX, 0);
|
|
ctx.lineTo(centerX, canvas.height);
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, centerY);
|
|
ctx.lineTo(canvas.width, centerY);
|
|
ctx.stroke();
|
|
|
|
// Draw locations
|
|
currentLocations.forEach(location => {
|
|
const screenX = centerX + location.x * scale;
|
|
const screenY = centerY - location.y * scale;
|
|
|
|
// Danger level colors
|
|
const dangerColors = ['#4caf50', '#8bc34a', '#ffa726', '#ff5722', '#d32f2f'];
|
|
const color = dangerColors[location.danger_level] || '#9e9e9e';
|
|
|
|
// Draw location circle
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.arc(screenX, screenY, 15, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Highlight selected
|
|
if (location.id === selectedLocationId) {
|
|
ctx.strokeStyle = '#ffa726';
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Draw label
|
|
ctx.fillStyle = '#e0e0e0';
|
|
ctx.font = '12px sans-serif';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(location.name, screenX, screenY + 30);
|
|
});
|
|
}
|
|
|
|
function handleCanvasClick(event) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const clickX = event.clientX - rect.left;
|
|
const clickY = event.clientY - rect.top;
|
|
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
|
|
// Check if clicked on existing location
|
|
for (const location of currentLocations) {
|
|
const screenX = centerX + location.x * scale;
|
|
const screenY = centerY - location.y * scale;
|
|
|
|
const distance = Math.sqrt(Math.pow(clickX - screenX, 2) + Math.pow(clickY - screenY, 2));
|
|
|
|
if (distance < 15) {
|
|
selectLocation(location.id);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Clicked on empty space - create new location
|
|
const worldX = ((clickX - centerX) / scale).toFixed(1);
|
|
const worldY = (-(clickY - centerY) / scale).toFixed(1);
|
|
|
|
if (confirm(`Create new location at (${worldX}, ${worldY})?`)) {
|
|
createLocationAt(parseFloat(worldX), parseFloat(worldY));
|
|
}
|
|
}
|
|
|
|
function createLocationAt(x, y) {
|
|
const newId = 'location_' + Date.now();
|
|
|
|
const newLocation = {
|
|
id: newId,
|
|
name: 'New Location',
|
|
description: 'Enter description...',
|
|
image_path: '',
|
|
x: x,
|
|
y: y,
|
|
danger_level: 0,
|
|
encounter_rate: 0.0,
|
|
wandering_chance: 0.0,
|
|
spawn_npcs: []
|
|
};
|
|
|
|
currentLocations.push(newLocation);
|
|
selectedLocationId = newId;
|
|
renderLocationList();
|
|
populateForm(newLocation);
|
|
drawMap();
|
|
}
|
|
|
|
function createNewLocation() {
|
|
createLocationAt(0, 0);
|
|
}
|
|
|
|
async function saveLocation() {
|
|
const locationData = {
|
|
id: document.getElementById('locationId').value,
|
|
name: document.getElementById('locationName').value,
|
|
description: document.getElementById('locationDescription').value,
|
|
x: parseFloat(document.getElementById('locationX').value),
|
|
y: parseFloat(document.getElementById('locationY').value),
|
|
danger_level: parseInt(document.getElementById('dangerLevel').value),
|
|
encounter_rate: parseFloat(document.getElementById('encounterRate').value),
|
|
wandering_chance: parseFloat(document.getElementById('wanderingChance').value),
|
|
image_path: document.getElementById('imagePath').value,
|
|
spawn_npcs: getCurrentSpawns()
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/editor/location', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(locationData)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showSuccess('Location saved successfully!');
|
|
await loadLocations();
|
|
drawMap();
|
|
} else {
|
|
alert('Failed to save: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Failed to save location: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function getCurrentSpawns() {
|
|
const spawns = [];
|
|
const spawnItems = document.querySelectorAll('.spawn-item');
|
|
|
|
spawnItems.forEach(item => {
|
|
const nameDiv = item.querySelector('.spawn-item-name');
|
|
const weightDiv = item.querySelector('.spawn-item-weight');
|
|
|
|
if (nameDiv && weightDiv) {
|
|
const text = nameDiv.textContent;
|
|
const npcName = text.substring(text.indexOf(' ') + 1).trim();
|
|
const weight = parseInt(weightDiv.textContent.replace('Weight: ', ''));
|
|
|
|
// Find NPC ID by name
|
|
const npc = availableNPCs.find(n => n.name === npcName);
|
|
if (npc) {
|
|
spawns.push({npc_id: npc.id, weight: weight});
|
|
}
|
|
}
|
|
});
|
|
|
|
return spawns;
|
|
}
|
|
|
|
function showAddSpawnModal() {
|
|
const modal = document.getElementById('addSpawnModal');
|
|
const list = document.getElementById('npcSelectList');
|
|
|
|
list.innerHTML = '';
|
|
|
|
availableNPCs.forEach(npc => {
|
|
const item = document.createElement('div');
|
|
item.className = 'npc-select-item';
|
|
item.innerHTML = `
|
|
<div><strong>${npc.emoji} ${npc.name}</strong></div>
|
|
<div style="font-size: 0.85em; opacity: 0.7;">
|
|
HP: ${npc.hp_range[0]}-${npc.hp_range[1]} |
|
|
DMG: ${npc.damage_range[0]}-${npc.damage_range[1]} |
|
|
XP: ${npc.xp_reward}
|
|
</div>
|
|
`;
|
|
|
|
item.onclick = () => addSpawn(npc);
|
|
list.appendChild(item);
|
|
});
|
|
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
function closeAddSpawnModal() {
|
|
document.getElementById('addSpawnModal').style.display = 'none';
|
|
}
|
|
|
|
function addSpawn(npc) {
|
|
const weight = prompt(`Enter spawn weight for ${npc.name}:`, '50');
|
|
if (weight && !isNaN(weight)) {
|
|
const list = document.getElementById('spawnList');
|
|
const item = document.createElement('div');
|
|
item.className = 'spawn-item';
|
|
item.innerHTML = `
|
|
<div class="spawn-item-info">
|
|
<div class="spawn-item-name">${npc.emoji} ${npc.name}</div>
|
|
<div class="spawn-item-weight">Weight: ${weight}</div>
|
|
</div>
|
|
<button class="btn btn-remove" onclick="this.parentElement.remove()">Remove</button>
|
|
`;
|
|
list.appendChild(item);
|
|
}
|
|
closeAddSpawnModal();
|
|
}
|
|
|
|
function removeSpawn(index) {
|
|
const spawnItems = document.querySelectorAll('.spawn-item');
|
|
if (spawnItems[index]) {
|
|
spawnItems[index].remove();
|
|
}
|
|
}
|
|
|
|
async function uploadImage() {
|
|
const fileInput = document.getElementById('imageUpload');
|
|
const file = fileInput.files[0];
|
|
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
try {
|
|
const response = await fetch('/api/editor/upload-image', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
document.getElementById('imagePath').value = data.image_path;
|
|
updateImagePreview(data.image_path);
|
|
showSuccess(data.message);
|
|
} else {
|
|
alert('Upload failed: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Upload failed: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await fetch('/api/logout', {method: 'POST'});
|
|
window.location.reload();
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
}
|