@ -2401,6 +2401,229 @@
left: 0;
}
/* Home Base Button */
.home-base-btn {
position: fixed;
bottom: 100px;
right: 15px;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #4a90d9 0%, #357abd 100%);
border: 3px solid #fff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
cursor: pointer;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
transition: all 0.2s;
}
.home-base-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.home-base-btn.selecting {
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(243, 156, 18, 0.7); }
50% { box-shadow: 0 0 0 15px rgba(243, 156, 18, 0); }
}
/* Home Base Marker */
.home-base-marker {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.home-base-marker img {
width: 50px;
height: 50px;
object-fit: contain;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
}
/* Death Overlay */
.death-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 2000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
text-align: center;
padding: 20px;
}
.death-overlay h1 {
font-size: 48px;
color: #e94560;
margin-bottom: 20px;
text-shadow: 0 0 20px rgba(233, 69, 96, 0.8);
}
.death-overlay p {
font-size: 18px;
margin-bottom: 10px;
max-width: 300px;
}
.death-overlay .xp-lost {
color: #e94560;
font-size: 24px;
margin: 20px 0;
}
.death-overlay .home-distance {
margin-top: 20px;
font-size: 16px;
color: #4ecdc4;
}
/* Dead state HUD styling */
.rpg-hud.dead {
filter: grayscale(100%);
opacity: 0.6;
}
.rpg-hud.dead::after {
content: '💀';
position: absolute;
top: -10px;
right: -10px;
font-size: 24px;
}
/* Home base selection mode hint */
.selection-hint {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(243, 156, 18, 0.95);
color: white;
padding: 12px 24px;
border-radius: 25px;
font-weight: bold;
z-index: 1001;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
/* Homebase Customization Modal */
.homebase-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 3000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.homebase-modal-content {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 16px;
padding: 24px;
max-width: 400px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.homebase-modal h2 {
color: #4ecdc4;
text-align: center;
margin-bottom: 20px;
font-size: 24px;
}
.homebase-modal h3 {
color: #fff;
margin: 16px 0 12px;
font-size: 16px;
}
.homebase-icons-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.homebase-icon-option {
aspect-ratio: 1;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
border: 3px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding: 8px;
}
.homebase-icon-option:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.homebase-icon-option.selected {
border-color: #4ecdc4;
background: rgba(78, 205, 196, 0.2);
}
.homebase-icon-option img {
width: 100%;
height: 100%;
object-fit: contain;
}
.homebase-modal-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.homebase-modal-actions button {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.homebase-btn-relocate {
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
color: white;
}
.homebase-btn-relocate:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4);
}
.homebase-btn-relocate:disabled {
background: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.homebase-btn-close {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.homebase-btn-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.homebase-relocate-cooldown {
text-align: center;
color: #888;
font-size: 12px;
margin-top: 8px;
}
< / style >
< / head >
< body >
@ -2432,6 +2655,38 @@
< / div >
< / div >
<!-- Home Base Button -->
< button id = "homeBaseBtn" class = "home-base-btn" style = "display: none;" onclick = "toggleHomeBaseSelection()" title = "Set Home Base" > 🏠< / button >
<!-- Home Base Selection Hint -->
< div id = "selectionHint" class = "selection-hint" style = "display: none;" > Tap on the map to set your home base< / div >
<!-- Death Overlay -->
< div id = "deathOverlay" class = "death-overlay" style = "display: none;" >
< h1 > 💀 YOU DIED 💀< / h1 >
< p > Return to your home base to respawn.< / p >
< div class = "xp-lost" id = "xpLostText" > -0 XP< / div >
< div class = "home-distance" id = "homeDistanceText" > Distance to home: ???< / div >
< / div >
<!-- Homebase Customization Modal -->
< div id = "homebaseModal" class = "homebase-modal" style = "display: none;" >
< div class = "homebase-modal-content" >
< h2 > 🏠 Homebase< / h2 >
< h3 > Select Icon< / h3 >
< div id = "homebaseIconsGrid" class = "homebase-icons-grid" >
<!-- Icons loaded dynamically -->
< / div >
< div class = "homebase-modal-actions" >
< button id = "relocateBtn" class = "homebase-btn-relocate" onclick = "startRelocateHomebase()" > Relocate< / button >
< button class = "homebase-btn-close" onclick = "closeHomebaseModal()" > Close< / button >
< / div >
< div id = "relocateCooldown" class = "homebase-relocate-cooldown" > < / div >
< / div >
< / div >
<!-- Combat Overlay -->
< div id = "combatOverlay" class = "combat-overlay" style = "display: none;" >
< div class = "combat-container" >
@ -3220,7 +3475,7 @@
// GPS test mode (admin only)
let gpsTestMode = false;
let testPosition = { lat: 37.7749, lng: -122.4194 }; // Default to SF
const GPS_TEST_STEP = 0.0001; // ~11 meters per step
const GPS_TEST_STEP = 0.000009; // ~1 meter per step
// Navigation state
let navMode = false;
@ -3461,6 +3716,9 @@
xpReward: t.xpReward,
accuracy: t.accuracy || 85,
dodge: t.dodge || 5,
minLevel: t.minLevel || 1,
maxLevel: t.maxLevel || 99,
spawnWeight: t.spawnWeight || 100,
levelScale: t.levelScale
};
MONSTER_DIALOGUES[t.id] = t.dialogues;
@ -3473,6 +3731,22 @@
}
}
// Load spawn settings from server
async function loadSpawnSettings() {
try {
const response = await fetch('/api/spawn-settings');
if (response.ok) {
const settings = await response.json();
spawnSettings.spawnInterval = settings.spawnInterval || 20000;
spawnSettings.spawnChance = settings.spawnChance || 50;
spawnSettings.spawnDistance = settings.spawnDistance || 10;
console.log('Loaded spawn settings:', spawnSettings);
}
} catch (err) {
console.error('Failed to load spawn settings:', err);
}
}
// Load skills from database
async function loadSkillsFromDatabase() {
try {
@ -3616,6 +3890,23 @@
let combatState = null; // Active combat state or null
let monsterSpawnTimer = null; // Interval for spawning monsters
let monsterUpdateTimer = null; // Interval for updating monster positions/dialogue
let lastSpawnLocation = null; // Track player location at last spawn (for movement-based spawning)
// Spawn settings (loaded from server, with defaults)
let spawnSettings = {
spawnInterval: 20000, // Timer interval in ms
spawnChance: 50, // Percent chance per interval
spawnDistance: 10 // Meters player must move
};
// Home Base state variables
let homeBaseMarker = null; // Leaflet marker for home base
let homeBaseSelectionMode = false; // Whether we're in home base selection mode
let xpLostOnDeath = 0; // Track XP lost for display
let lastHomeRegenTime = 0; // Track last HP/MP regen at home base
const HOME_REGEN_INTERVAL = 3000; // Regen every 3 seconds when at home
const HOME_REGEN_PERCENT = 5; // Regen 5% of max HP/MP per tick
const HOME_BASE_RADIUS = 20; // Meters - radius for home base effects
// Find nearest monster to a location (for double-tap to battle on mobile)
function findNearestMonster(latlng, maxDistanceMeters = 50) {
@ -3810,6 +4101,12 @@
// Store user location for geocache proximity checks
userLocation = { lat, lng, accuracy };
// Check if dead player has reached home base for respawn
checkHomeBaseRespawn();
// Check for HP/MP regeneration at home base
checkHomeBaseRegen();
// Update geocache visibility based on new location
if (navMode) {
updateGeocacheVisibility();
@ -7378,6 +7675,12 @@
// Map click handler
map.on('click', (e) => {
// Handle home base selection mode
if (homeBaseSelectionMode) {
setHomeBase(e.latlng.lat, e.latlng.lng);
return;
}
// In navigation mode, clicks are handled by press-and-hold
if (navMode) {
return;
@ -9394,15 +9697,53 @@
// Update skills (show only unlocked skills)
const unlockedSkills = playerStats.unlockedSkills || ['basic_attack'];
document.getElementById('charSheetSkills').innerHTML = unlockedSkills.map(skillId => {
const skill = SKILLS[skillId];
const dbSkill = SKILLS_DB[skillId];
const hardcodedSkill = SKILLS[skillId];
const skill = dbSkill || hardcodedSkill;
if (!skill) return '';
// Calculate skill stats for display
let statsText = '';
const accuracy = dbSkill?.accuracy || 95;
const mpCost = skill.mpCost || 0;
if (skill.type === 'damage') {
// Calculate damage based on player ATK
let damage;
const hits = skill.hitCount || skill.hits || 1;
if (hardcodedSkill & & hardcodedSkill.calculate) {
damage = hardcodedSkill.calculate(playerStats.atk);
} else if (dbSkill) {
damage = Math.floor(playerStats.atk * (dbSkill.basePower / 100));
} else {
damage = playerStats.atk;
}
const minDmg = Math.max(1, Math.floor(damage * 0.9));
const maxDmg = Math.floor(damage * 1.1);
if (hits > 1) {
statsText = `${minDmg}-${maxDmg} x${hits} | ${accuracy}% | ${mpCost} MP`;
} else {
statsText = `${minDmg}-${maxDmg} dmg | ${accuracy}% | ${mpCost} MP`;
}
} else if (skill.type === 'heal') {
const healAmount = hardcodedSkill?.calculate ? hardcodedSkill.calculate(playerStats.maxHp) : 0;
statsText = `+${healAmount} HP | ${mpCost} MP`;
} else if (skill.type === 'restore') {
const restoreAmount = hardcodedSkill?.calculate ? hardcodedSkill.calculate(playerStats.maxMp) : 0;
statsText = `+${restoreAmount} MP | ${mpCost} MP`;
} else if (skill.type === 'buff') {
statsText = `Buff | ${mpCost} MP`;
} else {
statsText = `${mpCost} MP`;
}
return `
< div class = "char-sheet-skill" >
< span class = "skill-icon" > ${skill.icon}< / span >
< div class = "skill-info" >
< div class = "skill-name" > ${skill.name}< / div >
< div class = "skill-desc" > ${skill.description}< / div >
< div class = "skill-cost" > ${skill.mpCost} MP< / div >
< div class = "skill-cost" > ${statsText} < / div >
< / div >
< / div >
`;
@ -9673,7 +10014,14 @@
// Show RPG HUD and start game
document.getElementById('rpgHud').style.display = 'flex';
updateRpgHud();
updateHomeBaseMarker();
// If player is dead, show death overlay
if (playerStats.isDead) {
document.getElementById('deathOverlay').style.display = 'flex';
} else {
startMonsterSpawning();
}
console.log('RPG system initialized for', username);
return;
@ -9694,7 +10042,14 @@
document.getElementById('rpgHud').style.display = 'flex';
updateRpgHud();
updateHomeBaseMarker();
// If player is dead, show death overlay
if (playerStats.isDead) {
document.getElementById('deathOverlay').style.display = 'flex';
} else {
startMonsterSpawning();
}
return;
}
} catch (e) {
@ -9770,6 +10125,442 @@
const xpPercent = Math.min(100, (playerStats.xp / xpNeeded) * 100);
document.getElementById('hudXpBar').style.width = xpPercent + '%';
document.getElementById('hudXpText').textContent = `${playerStats.xp}/${xpNeeded}`;
// Update dead state styling
const hud = document.getElementById('rpgHud');
if (playerStats.isDead) {
hud.classList.add('dead');
} else {
hud.classList.remove('dead');
}
// Show/hide home base button
document.getElementById('homeBaseBtn').style.display = playerStats ? 'flex' : 'none';
}
// ==========================================
// HOME BASE SYSTEM
// ==========================================
// Create or update home base marker on map
function updateHomeBaseMarker() {
if (!playerStats || !playerStats.homeBaseLat || !playerStats.homeBaseLng) {
if (homeBaseMarker) {
map.removeLayer(homeBaseMarker);
homeBaseMarker = null;
}
updateHomeBaseButton();
return;
}
// Use selected icon or default to '00' (use 100px, CSS scales to 50)
const iconId = playerStats.homeBaseIcon || '00';
const iconSrc = `/mapgameimgs/homebase${iconId}-100.png`;
const iconHtml = `
< div class = "home-base-marker" >
< img src = "${iconSrc}" alt = "Home Base" style = "width:50px;height:50px;"
onerror="this.src='/mapgameimgs/default50.png'">
< / div >
`;
const divIcon = L.divIcon({
html: iconHtml,
iconSize: [50, 50],
iconAnchor: [25, 25],
className: 'home-base-icon'
});
if (homeBaseMarker) {
// Update the icon (need to recreate the marker to change icon)
homeBaseMarker.setLatLng([playerStats.homeBaseLat, playerStats.homeBaseLng]);
homeBaseMarker.setIcon(divIcon);
} else {
homeBaseMarker = L.marker([playerStats.homeBaseLat, playerStats.homeBaseLng], {
icon: divIcon,
interactive: false
}).addTo(map);
}
updateHomeBaseButton();
}
// Update home base button text based on whether home is set
function updateHomeBaseButton() {
const btn = document.getElementById('homeBaseBtn');
if (playerStats & & playerStats.homeBaseLat & & playerStats.homeBaseLng) {
btn.innerHTML = '🏠';
btn.title = 'Homebase Settings';
} else {
btn.innerHTML = '🏠';
btn.title = 'Set Home Base';
}
}
// Toggle home base selection mode OR open customization modal
async function toggleHomeBaseSelection() {
// If in selection mode, cancel it
if (homeBaseSelectionMode) {
homeBaseSelectionMode = false;
document.getElementById('homeBaseBtn').classList.remove('selecting');
document.getElementById('selectionHint').style.display = 'none';
return;
}
// If home base is already set, open the customization modal
if (playerStats & & playerStats.homeBaseLat & & playerStats.homeBaseLng) {
openHomebaseModal();
return;
}
// Otherwise, start setting home base
await startSettingHomeBase();
}
// Start the home base selection process
async function startSettingHomeBase() {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/can-set-home', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (!data.canSet) {
alert('You can only set your home base once per day. Try again tomorrow!');
return;
}
// Enter selection mode
homeBaseSelectionMode = true;
document.getElementById('homeBaseBtn').classList.add('selecting');
document.getElementById('selectionHint').style.display = 'block';
} catch (err) {
console.error('Failed to check home base availability:', err);
}
}
// Open the homebase customization modal
async function openHomebaseModal() {
const modal = document.getElementById('homebaseModal');
modal.style.display = 'flex';
// Load available icons
await loadHomebaseIcons();
// Check if relocation is available
await updateRelocateButton();
}
// Close the homebase modal
function closeHomebaseModal() {
document.getElementById('homebaseModal').style.display = 'none';
}
// Load available homebase icons from server
async function loadHomebaseIcons() {
const grid = document.getElementById('homebaseIconsGrid');
grid.innerHTML = '< div style = "color: #888; text-align: center; grid-column: 1/-1;" > Loading icons...< / div > ';
try {
const response = await fetch('/api/homebase-icons');
const icons = await response.json();
if (icons.length === 0) {
grid.innerHTML = '< div style = "color: #888; text-align: center; grid-column: 1/-1;" > No homebase icons found< / div > ';
return;
}
grid.innerHTML = icons.map(icon => `
< div class = "homebase-icon-option ${playerStats.homeBaseIcon === icon.id ? 'selected' : ''}"
onclick="selectHomebaseIcon('${icon.id}')"
data-icon-id="${icon.id}">
< img src = "${icon.preview}" alt = "Homebase ${icon.id}"
onerror="this.src='/mapgameimgs/default50.png'">
< / div >
`).join('');
} catch (err) {
console.error('Failed to load homebase icons:', err);
grid.innerHTML = '< div style = "color: #e94560; text-align: center; grid-column: 1/-1;" > Failed to load icons< / div > ';
}
}
// Select a homebase icon
async function selectHomebaseIcon(iconId) {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/home-base/icon', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ iconId })
});
if (response.ok) {
playerStats.homeBaseIcon = iconId;
// Update selected state in UI
document.querySelectorAll('.homebase-icon-option').forEach(el => {
el.classList.toggle('selected', el.dataset.iconId === iconId);
});
// Update the marker on the map
updateHomeBaseMarker();
console.log('Home base icon updated to:', iconId);
}
} catch (err) {
console.error('Failed to update homebase icon:', err);
}
}
// Update the relocate button based on cooldown
async function updateRelocateButton() {
const btn = document.getElementById('relocateBtn');
const cooldownText = document.getElementById('relocateCooldown');
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/can-set-home', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.canSet) {
btn.disabled = false;
cooldownText.textContent = '';
} else {
btn.disabled = true;
// Calculate time remaining
if (playerStats.lastHomeSet) {
const lastSet = new Date(playerStats.lastHomeSet);
const nextAvailable = new Date(lastSet.getTime() + 24 * 60 * 60 * 1000);
const now = new Date();
const hoursLeft = Math.ceil((nextAvailable - now) / (1000 * 60 * 60));
cooldownText.textContent = `Available in ~${hoursLeft} hour${hoursLeft !== 1 ? 's' : ''}`;
} else {
cooldownText.textContent = 'Try again tomorrow';
}
}
} catch (err) {
console.error('Failed to check relocate availability:', err);
}
}
// Start relocating homebase
async function startRelocateHomebase() {
closeHomebaseModal();
await startSettingHomeBase();
}
// Set home base at the given location
async function setHomeBase(lat, lng) {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/home-base', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ lat, lng })
});
if (response.ok) {
const data = await response.json();
playerStats.homeBaseLat = data.homeBaseLat;
playerStats.homeBaseLng = data.homeBaseLng;
updateHomeBaseMarker();
console.log('Home base set at:', lat, lng);
} else {
const error = await response.json();
alert(error.error || 'Failed to set home base');
}
} catch (err) {
console.error('Failed to set home base:', err);
}
// Exit selection mode
homeBaseSelectionMode = false;
document.getElementById('homeBaseBtn').classList.remove('selecting');
document.getElementById('selectionHint').style.display = 'none';
}
// Calculate distance to home base in meters
function getDistanceToHome() {
if (!userLocation || !playerStats || !playerStats.homeBaseLat) return null;
const metersPerDegLat = 111320;
const metersPerDegLng = 111320 * Math.cos(userLocation.lat * Math.PI / 180);
const dx = (userLocation.lng - playerStats.homeBaseLng) * metersPerDegLng;
const dy = (userLocation.lat - playerStats.homeBaseLat) * metersPerDegLat;
return Math.sqrt(dx * dx + dy * dy);
}
// Check if player has reached home base for respawn
function checkHomeBaseRespawn() {
if (!playerStats || !playerStats.isDead) return;
if (!playerStats.homeBaseLat) return;
const distance = getDistanceToHome();
if (distance === null) return;
// Update distance display
const distanceText = distance < 1000
? `${Math.round(distance)}m to home`
: `${(distance / 1000).toFixed(1)}km to home`;
document.getElementById('homeDistanceText').textContent = distanceText;
// Respawn if within home base radius
if (distance < = HOME_BASE_RADIUS) {
respawnPlayer();
}
}
// Check for HP/MP regeneration at home base
function checkHomeBaseRegen() {
// Skip if dead, no home base, or no player stats
if (!playerStats || playerStats.isDead) return;
if (!playerStats.homeBaseLat) return;
// Check if at full HP and MP already
if (playerStats.hp >= playerStats.maxHp & & playerStats.mp >= playerStats.maxMp) return;
// Check distance to home
const distance = getDistanceToHome();
if (distance === null || distance > HOME_BASE_RADIUS) return;
// Check if enough time has passed since last regen
const now = Date.now();
if (now - lastHomeRegenTime < HOME_REGEN_INTERVAL ) return ;
// Regenerate HP and MP
let regenOccurred = false;
const hpRegen = Math.ceil(playerStats.maxHp * (HOME_REGEN_PERCENT / 100));
const mpRegen = Math.ceil(playerStats.maxMp * (HOME_REGEN_PERCENT / 100));
if (playerStats.hp < playerStats.maxHp ) {
playerStats.hp = Math.min(playerStats.maxHp, playerStats.hp + hpRegen);
regenOccurred = true;
}
if (playerStats.mp < playerStats.maxMp ) {
playerStats.mp = Math.min(playerStats.maxMp, playerStats.mp + mpRegen);
regenOccurred = true;
}
if (regenOccurred) {
lastHomeRegenTime = now;
updateRpgHud();
savePlayerStats();
// Show subtle regen indicator
showHomeRegenEffect();
}
}
// Show a subtle visual effect for home regen
function showHomeRegenEffect() {
const hud = document.getElementById('rpgHud');
if (!hud) return;
// Add a brief glow effect
hud.style.boxShadow = '0 0 15px rgba(76, 175, 80, 0.6)';
setTimeout(() => {
hud.style.boxShadow = '';
}, 500);
}
// Handle player death
async function handlePlayerDeath() {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/death', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
xpLostOnDeath = data.xpLost;
playerStats.xp = data.newXp;
playerStats.hp = 0;
playerStats.isDead = true;
// Clear local monster entourage
monsterEntourage.forEach(m => {
if (m.marker) map.removeLayer(m.marker);
});
monsterEntourage = [];
// Show death overlay
document.getElementById('xpLostText').textContent = `-${xpLostOnDeath} XP`;
document.getElementById('deathOverlay').style.display = 'flex';
// Update HUD
updateRpgHud();
console.log('Player died, lost', xpLostOnDeath, 'XP');
}
} catch (err) {
console.error('Failed to handle death:', err);
}
}
// Respawn player at home base
async function respawnPlayer() {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/respawn', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
playerStats.isDead = false;
playerStats.hp = data.hp;
playerStats.mp = data.mp;
// Hide death overlay
document.getElementById('deathOverlay').style.display = 'none';
// Update HUD
updateRpgHud();
savePlayerStats();
// Spawn a monster after a short delay
setTimeout(() => {
spawnMonsterNearPlayer();
}, 2000);
console.log('Player respawned with full HP/MP');
}
} catch (err) {
console.error('Failed to respawn:', err);
}
}
// Save monsters to server (debounced)
@ -9865,6 +10656,12 @@
monsterEntourage.push(monster);
}
updateRpgHud();
// Set last spawn location so player needs to move before more spawn
if (userLocation) {
lastSpawnLocation = { lat: userLocation.lat, lng: userLocation.lng };
}
return true;
}
}
@ -9882,12 +10679,13 @@
// First, try to load existing monsters
await loadMonsters();
// Spawn check every 20 seconds
// Spawn check based on settings (interval and chance)
monsterSpawnTimer = setInterval(() => {
if (Math.random() < 0.5 ) { / / 50 % chance each interval
const chanceRoll = Math.random() * 100;
if (chanceRoll < spawnSettings.spawnChance ) {
spawnMonsterNearPlayer();
}
}, 20000 );
}, spawnSettings.spawnInterval );
// Update monster positions and dialogue every 2 seconds
monsterUpdateTimer = setInterval(() => {
@ -9917,9 +10715,25 @@
// Spawn a monster near the player
function spawnMonsterNearPlayer() {
if (!userLocation || !playerStats) return;
if (playerStats.isDead) return; // Don't spawn when dead
if (monsterEntourage.length >= getMaxMonsters()) return;
if (!monsterTypesLoaded || Object.keys(MONSTER_TYPES).length === 0) return;
// Movement-based spawning: first monster can spawn standing still,
// but subsequent monsters require player to move the configured distance
if (monsterEntourage.length > 0 & & lastSpawnLocation) {
const metersPerDegLat = 111320;
const metersPerDegLng = 111320 * Math.cos(userLocation.lat * Math.PI / 180);
const dx = (userLocation.lng - lastSpawnLocation.lng) * metersPerDegLng;
const dy = (userLocation.lat - lastSpawnLocation.lat) * metersPerDegLat;
const distanceMoved = Math.sqrt(dx * dx + dy * dy);
if (distanceMoved < spawnSettings.spawnDistance ) {
// Player hasn't moved far enough yet, skip spawn
return;
}
}
// Pick a random monster type from available types
const typeIds = Object.keys(MONSTER_TYPES);
const typeId = typeIds[Math.floor(Math.random() * typeIds.length)];
@ -9936,7 +10750,11 @@
const offsetLat = (distance * Math.cos(angle)) / metersPerDegLat;
const offsetLng = (distance * Math.sin(angle)) / metersPerDegLng;
const monsterLevel = Math.max(1, playerStats.level + Math.floor(Math.random() * 3) - 1);
// Calculate monster level based on player level, but respect monster type's min/max level
const baseLevel = Math.max(1, playerStats.level + Math.floor(Math.random() * 3) - 1);
const minLevel = monsterType.minLevel || 1;
const maxLevel = monsterType.maxLevel || 99;
const monsterLevel = Math.max(minLevel, Math.min(maxLevel, baseLevel));
const monster = {
id: `monster_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
@ -9960,6 +10778,9 @@
updateRpgHud();
saveMonsters(); // Persist to server
// Update last spawn location for movement-based spawning
lastSpawnLocation = { lat: userLocation.lat, lng: userLocation.lng };
console.log('Spawned monster:', monster.id, 'at level', monsterLevel);
}
@ -10091,6 +10912,7 @@
if (combatState) return; // Already in combat
if (!playerStats) return;
if (monsterEntourage.length === 0) return;
if (playerStats.isDead) return; // Can't fight when dead
// Load skills for each unique monster type
const uniqueTypes = [...new Set(monsterEntourage.map(m => m.type))];
@ -10775,9 +11597,21 @@
// Handle combat defeat
function handleCombatDefeat() {
const monsterCount = combatState.monsters.filter(m => m.hp > 0).length;
// If player has a home base, trigger death system
if (playerStats.homeBaseLat & & playerStats.homeBaseLng) {
addCombatLog(`💀 You have been slain! Return to your home base to respawn.`, 'damage');
playerStats.mp = combatState.player.mp;
// Close combat first, then handle death
setTimeout(() => {
closeCombatUI();
handlePlayerDeath();
}, 1500);
} else {
// No home base - use old behavior (restore HP to 50%)
addCombatLog(`You were defeated! ${monsterCount} ${monsterCount === 1 ? 'enemy remains' : 'enemies remain'}. HP restored to 50%.`, 'damage');
// Restore HP to 50%
playerStats.hp = Math.floor(playerStats.maxHp * 0.5);
playerStats.mp = combatState.player.mp;
@ -10786,6 +11620,7 @@
setTimeout(closeCombatUI, 2500);
}
}
// Flee from combat
function fleeCombat() {
@ -10840,8 +11675,8 @@
// END RPG COMBAT SYSTEM FUNCTIONS
// ==========================================
// Load monster types and skill s from database, then initialize auth
Promise.all([loadMonsterTypes(), loadSkillsFromDatabase()]).then(() => {
// Load monster types, skills, and spawn setting s from database, then initialize auth
Promise.all([loadMonsterTypes(), loadSkillsFromDatabase(), loadSpawnSettings() ]).then(() => {
loadCurrentUser();
});