diff --git a/admin.html b/admin.html index 4237185..c9ae550 100644 --- a/admin.html +++ b/admin.html @@ -762,12 +762,26 @@

Monster Spawning

- - + + + How often spawn attempts occur +
+
+ + + Percent chance per interval +
+
+
+
+ + + Distance player must move for new spawns
- + + Maximum monsters following player
@@ -1845,8 +1859,11 @@ const data = await api('/api/admin/settings'); settings = data.settings || {}; - // Populate form - document.getElementById('setting-monsterSpawnInterval').value = settings.monsterSpawnInterval || 30000; + // Populate form (convert interval from ms to seconds for display) + const intervalMs = settings.monsterSpawnInterval || 20000; + document.getElementById('setting-monsterSpawnInterval').value = Math.round(intervalMs / 1000); + document.getElementById('setting-monsterSpawnChance').value = settings.monsterSpawnChance || 50; + document.getElementById('setting-monsterSpawnDistance').value = settings.monsterSpawnDistance || 10; document.getElementById('setting-maxMonstersPerPlayer').value = settings.maxMonstersPerPlayer || 10; document.getElementById('setting-xpMultiplier').value = settings.xpMultiplier || 1.0; document.getElementById('setting-combatEnabled').checked = settings.combatEnabled !== 'false' && settings.combatEnabled !== false; @@ -1856,10 +1873,14 @@ } document.getElementById('saveSettingsBtn').addEventListener('click', async () => { + // Convert interval from seconds to ms for storage + const intervalSeconds = parseInt(document.getElementById('setting-monsterSpawnInterval').value) || 20; const newSettings = { - monsterSpawnInterval: document.getElementById('setting-monsterSpawnInterval').value, - maxMonstersPerPlayer: document.getElementById('setting-maxMonstersPerPlayer').value, - xpMultiplier: document.getElementById('setting-xpMultiplier').value, + monsterSpawnInterval: intervalSeconds * 1000, + monsterSpawnChance: parseInt(document.getElementById('setting-monsterSpawnChance').value) || 50, + monsterSpawnDistance: parseInt(document.getElementById('setting-monsterSpawnDistance').value) || 10, + maxMonstersPerPlayer: parseInt(document.getElementById('setting-maxMonstersPerPlayer').value) || 10, + xpMultiplier: parseFloat(document.getElementById('setting-xpMultiplier').value) || 1.0, combatEnabled: document.getElementById('setting-combatEnabled').checked }; diff --git a/database.js b/database.js index 58840b1..56453d9 100644 --- a/database.js +++ b/database.js @@ -209,6 +209,23 @@ class HikeMapDB { this.db.exec(`ALTER TABLE monster_skills ADD COLUMN custom_name TEXT`); } catch (e) { /* Column already exists */ } + // Migration: Add home base and death system columns to rpg_stats + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN home_base_lat REAL`); + } catch (e) { /* Column already exists */ } + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN home_base_lng REAL`); + } catch (e) { /* Column already exists */ } + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN last_home_set TEXT`); + } catch (e) { /* Column already exists */ } + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN is_dead INTEGER DEFAULT 0`); + } catch (e) { /* Column already exists */ } + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN home_base_icon TEXT DEFAULT '00'`); + } catch (e) { /* Column already exists */ } + // Game settings table - key/value store for game configuration this.db.exec(` CREATE TABLE IF NOT EXISTS game_settings ( @@ -468,7 +485,8 @@ class HikeMapDB { // RPG Stats methods getRpgStats(userId) { const stmt = this.db.prepare(` - SELECT character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills + SELECT character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills, + home_base_lat, home_base_lng, last_home_set, is_dead, home_base_icon FROM rpg_stats WHERE user_id = ? `); return stmt.get(userId); @@ -525,8 +543,8 @@ class HikeMapDB { saveRpgStats(userId, stats) { const stmt = this.db.prepare(` - INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills, home_base_lat, home_base_lng, last_home_set, is_dead, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(user_id) DO UPDATE SET character_name = COALESCE(excluded.character_name, rpg_stats.character_name), race = COALESCE(excluded.race, rpg_stats.race), @@ -542,6 +560,10 @@ class HikeMapDB { accuracy = excluded.accuracy, dodge = excluded.dodge, unlocked_skills = COALESCE(excluded.unlocked_skills, rpg_stats.unlocked_skills), + home_base_lat = COALESCE(excluded.home_base_lat, rpg_stats.home_base_lat), + home_base_lng = COALESCE(excluded.home_base_lng, rpg_stats.home_base_lng), + last_home_set = COALESCE(excluded.last_home_set, rpg_stats.last_home_set), + is_dead = COALESCE(excluded.is_dead, rpg_stats.is_dead), updated_at = datetime('now') `); // Convert unlockedSkills array to JSON string for storage @@ -561,10 +583,94 @@ class HikeMapDB { stats.def || 8, stats.accuracy || 90, stats.dodge || 10, - unlockedSkillsJson + unlockedSkillsJson, + stats.homeBaseLat || null, + stats.homeBaseLng || null, + stats.lastHomeSet || null, + stats.isDead !== undefined ? (stats.isDead ? 1 : 0) : null ); } + // Set home base location + setHomeBase(userId, lat, lng) { + const stmt = this.db.prepare(` + UPDATE rpg_stats SET + home_base_lat = ?, + home_base_lng = ?, + last_home_set = datetime('now'), + updated_at = datetime('now') + WHERE user_id = ? + `); + return stmt.run(lat, lng, userId); + } + + // Update home base icon + setHomeBaseIcon(userId, iconId) { + const stmt = this.db.prepare(` + UPDATE rpg_stats SET + home_base_icon = ?, + updated_at = datetime('now') + WHERE user_id = ? + `); + return stmt.run(iconId, userId); + } + + // Check if user can set home base (once per day) + canSetHomeBase(userId) { + const stmt = this.db.prepare(` + SELECT last_home_set FROM rpg_stats WHERE user_id = ? + `); + const result = stmt.get(userId); + if (!result || !result.last_home_set) return true; + + const lastSet = new Date(result.last_home_set); + const now = new Date(); + const hoursSince = (now - lastSet) / (1000 * 60 * 60); + return hoursSince >= 24; + } + + // Handle player death + handlePlayerDeath(userId, xpPenaltyPercent = 10) { + // Get current stats to calculate XP penalty + const stats = this.getRpgStats(userId); + if (!stats) return null; + + // Calculate XP loss - can't drop below current level threshold + const currentLevel = stats.level; + const levelThresholds = [0, 100, 250, 500, 800, 1200]; // XP needed for each level + const minXp = levelThresholds[currentLevel - 1] || 0; + const xpLoss = Math.floor(stats.xp * (xpPenaltyPercent / 100)); + const newXp = Math.max(minXp, stats.xp - xpLoss); + + const stmt = this.db.prepare(` + UPDATE rpg_stats SET + is_dead = 1, + hp = 0, + xp = ?, + updated_at = datetime('now') + WHERE user_id = ? + `); + stmt.run(newXp, userId); + + return { xpLost: stats.xp - newXp, newXp }; + } + + // Respawn player at home base + respawnPlayer(userId) { + const stats = this.getRpgStats(userId); + if (!stats) return null; + + const stmt = this.db.prepare(` + UPDATE rpg_stats SET + is_dead = 0, + hp = max_hp, + mp = max_mp, + updated_at = datetime('now') + WHERE user_id = ? + `); + return stmt.run(userId); + } + // Monster entourage methods getMonsterEntourage(userId) { const stmt = this.db.prepare(` @@ -611,6 +717,11 @@ class HikeMapDB { return stmt.run(userId, monsterId); } + clearMonsterEntourage(userId) { + const stmt = this.db.prepare(`DELETE FROM monster_entourage WHERE user_id = ?`); + return stmt.run(userId); + } + // Monster type methods getAllMonsterTypes(enabledOnly = true) { const stmt = enabledOnly @@ -1193,7 +1304,9 @@ class HikeMapDB { seedDefaultSettings() { const defaults = { - monsterSpawnInterval: 30000, + monsterSpawnInterval: 20000, // Timer interval in ms (20 seconds) + monsterSpawnChance: 50, // Percent chance per interval (50%) + monsterSpawnDistance: 10, // Meters player must move for new spawns (10m) maxMonstersPerPlayer: 10, xpMultiplier: 1.0, combatEnabled: true diff --git a/index.html b/index.html index cf80a49..d3d5d2f 100644 --- a/index.html +++ b/index.html @@ -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; + } + @@ -2432,6 +2655,38 @@ + + + + + + + + + + + +