Initial commit: Echoes of the Ashes - Telegram RPG Bot
This commit is contained in:
470
web-map/editor.js
Normal file
470
web-map/editor.js
Normal file
@@ -0,0 +1,470 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user