@@ -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 `
${skill.icon}
${skill.name}
${skill.description}
-
${skill.mpCost} MP
+
${statsText}
`;
@@ -9673,7 +10014,14 @@
// Show RPG HUD and start game
document.getElementById('rpgHud').style.display = 'flex';
updateRpgHud();
- startMonsterSpawning();
+ 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();
- startMonsterSpawning();
+ 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 = `
+
+

+
+ `;
+
+ 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 = '
Loading icons...
';
+
+ try {
+ const response = await fetch('/api/homebase-icons');
+ const icons = await response.json();
+
+ if (icons.length === 0) {
+ grid.innerHTML = '
No homebase icons found
';
+ return;
+ }
+
+ grid.innerHTML = icons.map(icon => `
+
+

+
+ `).join('');
+ } catch (err) {
+ console.error('Failed to load homebase icons:', err);
+ grid.innerHTML = '
Failed to load icons
';
+ }
+ }
+
+ // 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,16 +11597,29 @@
// Handle combat defeat
function handleCombatDefeat() {
const monsterCount = combatState.monsters.filter(m => m.hp > 0).length;
- 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;
+ // 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;
- savePlayerStats();
- updateRpgHud();
+ // 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');
- setTimeout(closeCombatUI, 2500);
+ playerStats.hp = Math.floor(playerStats.maxHp * 0.5);
+ playerStats.mp = combatState.player.mp;
+
+ savePlayerStats();
+ updateRpgHud();
+
+ setTimeout(closeCombatUI, 2500);
+ }
}
// Flee from combat
@@ -10840,8 +11675,8 @@
// END RPG COMBAT SYSTEM FUNCTIONS
// ==========================================
- // Load monster types and skills from database, then initialize auth
- Promise.all([loadMonsterTypes(), loadSkillsFromDatabase()]).then(() => {
+ // Load monster types, skills, and spawn settings from database, then initialize auth
+ Promise.all([loadMonsterTypes(), loadSkillsFromDatabase(), loadSpawnSettings()]).then(() => {
loadCurrentUser();
});
diff --git a/server.js b/server.js
index 29f8ee8..c00c90b 100644
--- a/server.js
+++ b/server.js
@@ -777,7 +777,12 @@ app.get('/api/user/rpg-stats', authenticateToken, (req, res) => {
def: stats.def,
accuracy: stats.accuracy || 90,
dodge: stats.dodge || 10,
- unlockedSkills: unlockedSkills
+ unlockedSkills: unlockedSkills,
+ homeBaseLat: stats.home_base_lat,
+ homeBaseLng: stats.home_base_lng,
+ lastHomeSet: stats.last_home_set,
+ isDead: !!stats.is_dead,
+ homeBaseIcon: stats.home_base_icon || '00'
});
} else {
// No stats yet - return null so client creates defaults
@@ -849,6 +854,150 @@ app.put('/api/user/rpg-stats', authenticateToken, (req, res) => {
}
});
+// Check if user can set home base (once per day)
+app.get('/api/user/can-set-home', authenticateToken, (req, res) => {
+ try {
+ const canSet = db.canSetHomeBase(req.user.userId);
+ res.json({ canSet });
+ } catch (err) {
+ console.error('Check home base error:', err);
+ res.status(500).json({ error: 'Failed to check home base availability' });
+ }
+});
+
+// Set home base location
+app.post('/api/user/home-base', authenticateToken, (req, res) => {
+ try {
+ const { lat, lng } = req.body;
+
+ if (lat === undefined || lng === undefined) {
+ return res.status(400).json({ error: 'Latitude and longitude are required' });
+ }
+
+ // Check if user can set home base (once per day)
+ if (!db.canSetHomeBase(req.user.userId)) {
+ return res.status(400).json({ error: 'You can only set your home base once per day' });
+ }
+
+ db.setHomeBase(req.user.userId, lat, lng);
+ res.json({ success: true, homeBaseLat: lat, homeBaseLng: lng });
+ } catch (err) {
+ console.error('Set home base error:', err);
+ res.status(500).json({ error: 'Failed to set home base' });
+ }
+});
+
+// Get available homebase icons (auto-detected from mapgameimgs directory)
+app.get('/api/homebase-icons', (req, res) => {
+ try {
+ const fs = require('fs');
+ const imagesDir = path.join(__dirname, 'mapgameimgs');
+
+ // Read directory and find homebaseXX-100.png files
+ const files = fs.readdirSync(imagesDir);
+ const iconPattern = /^homebase(\d+)-100\.png$/;
+
+ const icons = files
+ .map(file => {
+ const match = file.match(iconPattern);
+ if (match) {
+ return {
+ id: match[1],
+ filename: file,
+ preview: `/mapgameimgs/${file}`, // Use 100px, CSS scales down
+ full: `/mapgameimgs/${file}`
+ };
+ }
+ return null;
+ })
+ .filter(Boolean)
+ .sort((a, b) => a.id.localeCompare(b.id));
+
+ res.json(icons);
+ } catch (err) {
+ console.error('Get homebase icons error:', err);
+ res.status(500).json({ error: 'Failed to get homebase icons' });
+ }
+});
+
+// Get spawn settings (public - client needs these for spawn logic)
+app.get('/api/spawn-settings', (req, res) => {
+ try {
+ const settings = {
+ spawnInterval: JSON.parse(db.getSetting('monsterSpawnInterval') || '20000'),
+ spawnChance: JSON.parse(db.getSetting('monsterSpawnChance') || '50'),
+ spawnDistance: JSON.parse(db.getSetting('monsterSpawnDistance') || '10')
+ };
+ res.json(settings);
+ } catch (err) {
+ console.error('Get spawn settings error:', err);
+ res.status(500).json({ error: 'Failed to get spawn settings' });
+ }
+});
+
+// Update home base icon
+app.put('/api/user/home-base/icon', authenticateToken, (req, res) => {
+ try {
+ const { iconId } = req.body;
+
+ if (!iconId) {
+ return res.status(400).json({ error: 'Icon ID is required' });
+ }
+
+ db.setHomeBaseIcon(req.user.userId, iconId);
+ res.json({ success: true, homeBaseIcon: iconId });
+ } catch (err) {
+ console.error('Set home base icon error:', err);
+ res.status(500).json({ error: 'Failed to set home base icon' });
+ }
+});
+
+// Handle player death
+app.post('/api/user/death', authenticateToken, (req, res) => {
+ try {
+ const result = db.handlePlayerDeath(req.user.userId, 10); // 10% XP penalty
+ if (!result) {
+ return res.status(404).json({ error: 'Player stats not found' });
+ }
+
+ // Clear monster entourage
+ db.clearMonsterEntourage(req.user.userId);
+
+ res.json({
+ success: true,
+ xpLost: result.xpLost,
+ newXp: result.newXp
+ });
+ } catch (err) {
+ console.error('Handle death error:', err);
+ res.status(500).json({ error: 'Failed to handle death' });
+ }
+});
+
+// Respawn player at home base
+app.post('/api/user/respawn', authenticateToken, (req, res) => {
+ try {
+ const stats = db.getRpgStats(req.user.userId);
+ if (!stats) {
+ return res.status(404).json({ error: 'Player stats not found' });
+ }
+
+ if (!stats.is_dead) {
+ return res.status(400).json({ error: 'Player is not dead' });
+ }
+
+ db.respawnPlayer(req.user.userId);
+ res.json({
+ success: true,
+ hp: stats.max_hp,
+ mp: stats.max_mp
+ });
+ } catch (err) {
+ console.error('Respawn error:', err);
+ res.status(500).json({ error: 'Failed to respawn' });
+ }
+});
+
// Get all monster types (public endpoint - needed for game rendering)
app.get('/api/monster-types', (req, res) => {
try {
@@ -864,6 +1013,9 @@ app.get('/api/monster-types', (req, res) => {
xpReward: t.xp_reward,
accuracy: t.accuracy || 85,
dodge: t.dodge || 5,
+ minLevel: t.min_level || 1,
+ maxLevel: t.max_level || 99,
+ spawnWeight: t.spawn_weight || 100,
levelScale: {
hp: t.level_scale_hp,
atk: t.level_scale_atk,
diff --git a/to_do.md b/to_do.md
index d7bc428..6d254cf 100644
--- a/to_do.md
+++ b/to_do.md
@@ -45,15 +45,46 @@
- [ ] Add equipment UI to character sheet
- [ ] Calculate effective stats with equipment bonuses
-## Phase 6: Admin Editor
-- [ ] Create admin.html (separate page)
-- [ ] Add admin authentication middleware
+## Phase 6: Admin Editor - MOSTLY COMPLETE
+- [x] Create admin.html (separate page)
+- [x] Add admin authentication middleware
- [ ] User management (list, edit stats, grant admin)
- [x] Monster types stored in database (monster_types table created)
-- [ ] Monster management UI (CRUD for monster_types)
+- [x] Monster management UI (CRUD for monster_types)
+- [x] Monster cloning
+- [x] Monster enable/disable toggle
+- [x] Auto-copy default images for new monsters
- [ ] Spawn control (manual monster spawning)
- [ ] Game balance settings
+## Phase 8: Home Base / Death System - COMPLETE
+- [x] Add home_base_lat, home_base_lng, last_home_set, is_dead columns to rpg_stats
+- [x] Add "Set Home Base" mode - tap on map to select location
+- [x] Limit home base setting to once per day (check last_home_set timestamp)
+- [x] Show home base marker on map (uses default50.png)
+- [x] On death:
+ - [x] Set player state to "dead" (can't initiate combat)
+ - [x] All monsters despawn (clear entourage)
+ - [x] Lose 10% of XP (but cannot drop below current level threshold)
+- [x] Show "You are dead! Return to home base to respawn" message
+- [x] Visual indicator when dead (grayed HUD with skull icon)
+- [x] Detect when dead player reaches home base (~20m radius)
+- [x] Respawn player with full HP and MP when they reach home
+- [x] If no home base set, old behavior (restore 50% HP on defeat)
+
+## Phase 7: Skill Database System - COMPLETE
+- [x] Skills table in database
+- [x] Skills admin page (CRUD)
+- [x] Hit/miss mechanics (accuracy vs dodge)
+- [x] Monster skills with weighted random selection
+- [x] Custom skill names per monster
+- [x] Status effects (poison) with turn-based damage
+- [x] Buff skills (defend) working properly
+- [x] Status effect visual overlays (100x100px)
+- [x] Monster min/max level spawning
+- [ ] Class-specific skill names (getSkillForClass)
+- [ ] Class skill names admin editor
+
---
## Completed