diff --git a/admin.html b/admin.html index 6d1b78b..c7ff222 100644 --- a/admin.html +++ b/admin.html @@ -1523,6 +1523,8 @@ + +
@@ -2420,7 +2422,9 @@ 'atk_boost_percent': 'ATK %', 'def_boost_flat': 'DEF +', 'def_boost_percent': 'DEF %', - 'xp_multiplier': 'XP' + 'xp_multiplier': 'XP', + 'explore_radius_multiplier': 'Explore Radius', + 'homebase_radius_multiplier': 'Homebase Radius' }; tbody.innerHTML = utilitySkills.map(s => { diff --git a/index.html b/index.html index 8c9a594..388e80c 100644 --- a/index.html +++ b/index.html @@ -4267,8 +4267,9 @@ const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng); const centerPoint = map.latLngToContainerPoint(homeLatLng); - // Calculate radius in pixels using map projection - const edgeLatLng = destinationPoint(homeLatLng, playerRevealRadius, 90); + // Calculate radius in pixels using map projection (with multiplier) + const effectiveHomebaseRadius = playerRevealRadius * homebaseRadiusMultiplier; + const edgeLatLng = destinationPoint(homeLatLng, effectiveHomebaseRadius, 90); const edgePoint = map.latLngToContainerPoint(edgeLatLng); const radiusPixels = Math.abs(edgePoint.x - centerPoint.x); @@ -4297,16 +4298,19 @@ const playerLatLng = L.latLng(userLocation.lat, userLocation.lng); // Check if player is outside homebase radius (or no homebase) + // Use effective homebase radius with multiplier + const effectiveHomebaseRadius = playerRevealRadius * homebaseRadiusMultiplier; let isOutsideHomebase = true; if (playerStats && playerStats.homeBaseLat != null && playerStats.homeBaseLng != null) { const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng); const distToHome = playerLatLng.distanceTo(homeLatLng); - isOutsideHomebase = distToHome > playerRevealRadius; + isOutsideHomebase = distToHome > effectiveHomebaseRadius; } if (isOutsideHomebase) { const playerPoint = map.latLngToContainerPoint(playerLatLng); - const playerEdge = destinationPoint(playerLatLng, playerExploreRadius, 90); + const effectiveExploreRadius = playerExploreRadius * exploreRadiusMultiplier; + const playerEdge = destinationPoint(playerLatLng, effectiveExploreRadius, 90); const playerEdgePoint = map.latLngToContainerPoint(playerEdge); const playerRadiusPixels = Math.abs(playerEdgePoint.x - playerPoint.x); @@ -4334,18 +4338,20 @@ function isInRevealedArea(lat, lng) { const checkPoint = L.latLng(lat, lng); - // Check homebase radius + // Check homebase radius (with multiplier) if (playerStats && playerStats.homeBaseLat != null && playerStats.homeBaseLng != null) { const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng); - if (homeLatLng.distanceTo(checkPoint) <= playerRevealRadius) { + const effectiveHomebaseRadius = playerRevealRadius * homebaseRadiusMultiplier; + if (homeLatLng.distanceTo(checkPoint) <= effectiveHomebaseRadius) { return true; } } - // Check player's explore radius + // Check player's explore radius (with multiplier) if (userLocation) { const playerLatLng = L.latLng(userLocation.lat, userLocation.lng); - if (playerLatLng.distanceTo(checkPoint) <= playerExploreRadius) { + const effectiveExploreRadius = playerExploreRadius * exploreRadiusMultiplier; + if (playerLatLng.distanceTo(checkPoint) <= effectiveExploreRadius) { return true; } } @@ -5181,6 +5187,8 @@ // Player buffs state (loaded from server) let playerBuffs = {}; // Buff status keyed by buffType let mpRegenMultiplier = 1.0; // Current MP regen multiplier + let exploreRadiusMultiplier = 1.0; // Player explore radius multiplier (fog of war) + let homebaseRadiusMultiplier = 1.0; // Homebase reveal radius multiplier (fog of war) // ========================================== // MUSIC SYSTEM @@ -12365,107 +12373,143 @@ const container = document.getElementById('charSheetDailySkills'); if (!container) return; - // Check if player has Second Wind unlocked const unlockedSkills = playerStats?.unlockedSkills || ['basic_attack']; - const hasSecondWind = unlockedSkills.includes('second_wind'); - if (!hasSecondWind) { + // Find all unlocked utility skills + const utilitySkills = unlockedSkills.filter(skillId => { + const skill = SKILLS_DB[skillId] || SKILLS[skillId]; + return skill && skill.type === 'utility'; + }); + + if (utilitySkills.length === 0) { container.innerHTML = '
No daily skills unlocked yet
'; return; } - // Get Second Wind buff status - const buff = playerBuffs['second_wind']; - let statusClass = 'available'; - let statusText = 'Ready to use'; - let buttonClass = 'activate'; - let buttonText = 'Activate'; - let buttonDisabled = false; - - if (buff) { - if (buff.isActive) { - statusClass = 'active'; - statusText = `Active - ${formatTimeRemaining(buff.expiresIn)} remaining`; - buttonClass = 'active'; - buttonText = 'Active'; - buttonDisabled = true; - } else if (buff.isOnCooldown) { - statusClass = 'cooldown'; - statusText = `On cooldown - ${formatTimeRemaining(buff.cooldownEndsIn)}`; - buttonClass = 'cooldown'; - buttonText = 'Cooldown'; - buttonDisabled = true; - } - } - - const secondWindIconHtml = renderSkillIcon('second_wind', 'class', playerStats?.class, 24); - container.innerHTML = ` -
- ${secondWindIconHtml} -
-
Second Wind
-
Double MP regen while walking for 1 hour
-
${statusText}
+ container.innerHTML = utilitySkills.map(skillId => { + const skill = SKILLS_DB[skillId] || SKILLS[skillId]; + const buff = playerBuffs[skillId]; + + let statusClass = 'available'; + let statusText = 'Ready to use'; + let buttonClass = 'activate'; + let buttonText = 'Activate'; + let buttonDisabled = false; + + if (buff) { + if (buff.isActive) { + statusClass = 'active'; + statusText = `Active - ${formatTimeRemaining(buff.expiresIn)} remaining`; + buttonClass = 'active'; + buttonText = 'Active'; + buttonDisabled = true; + } else if (buff.isOnCooldown) { + statusClass = 'cooldown'; + statusText = `On cooldown - ${formatTimeRemaining(buff.cooldownEndsIn)}`; + buttonClass = 'cooldown'; + buttonText = 'Cooldown'; + buttonDisabled = true; + } + } + + const iconHtml = renderSkillIcon(skillId, 'class', playerStats?.class, 24); + const displayName = skill.name; + const displayDesc = skill.description; + + return ` +
+ ${iconHtml} +
+
${displayName}
+
${displayDesc}
+
${statusText}
+
+
- -
- `; + `; + }).join(''); } - // Activate Second Wind buff - async function activateSecondWind() { - console.log('activateSecondWind called'); + // Activate any utility skill buff + async function activateUtilitySkill(skillId) { + console.log('activateUtilitySkill called:', skillId); const token = localStorage.getItem('accessToken'); if (!token) { - console.error('No access token for Second Wind activation'); showNotification('Please log in to use skills', 'error'); return; } + const skill = SKILLS_DB[skillId] || SKILLS[skillId]; + if (!skill) { + showNotification('Unknown skill', 'error'); + return; + } + try { - console.log('Sending buff activation request...'); const response = await fetch('/api/user/buffs/activate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - body: JSON.stringify({ buffType: 'second_wind' }) + body: JSON.stringify({ buffType: skillId }) }); if (response.ok) { const data = await response.json(); + // Update local buff state - playerBuffs['second_wind'] = { + playerBuffs[skillId] = { isActive: true, isOnCooldown: true, expiresIn: data.expiresIn, cooldownEndsIn: data.cooldownEndsIn, + effectType: data.effectType, effectValue: data.effectValue }; - // Update multiplier - mpRegenMultiplier = data.effectValue; - updateStatus('Second Wind activated! MP regen doubled for 1 hour.', 'success'); + // Apply effect based on type + switch (data.effectType) { + case 'mp_regen_multiplier': + mpRegenMultiplier = data.effectValue; + break; + case 'explore_radius_multiplier': + exploreRadiusMultiplier = data.effectValue; + updateFogOfWar(); + updateGeocacheVisibility(); + break; + case 'homebase_radius_multiplier': + homebaseRadiusMultiplier = data.effectValue; + updateFogOfWar(); + updateGeocacheVisibility(); + break; + } + + updateStatus(`${skill.name} activated!`, 'success'); updateDailySkillsSection(); } else { const error = await response.json(); if (error.cooldownEndsIn) { - updateStatus(`Second Wind on cooldown for ${formatTimeRemaining(error.cooldownEndsIn)}`, 'error'); + updateStatus(`${skill.name} on cooldown for ${formatTimeRemaining(error.cooldownEndsIn)}`, 'error'); } else { - updateStatus(error.error || 'Failed to activate Second Wind', 'error'); + updateStatus(error.error || `Failed to activate ${skill.name}`, 'error'); } } } catch (err) { - console.error('Error activating Second Wind:', err); - updateStatus('Failed to activate Second Wind', 'error'); + console.error('Error activating utility skill:', err); + updateStatus(`Failed to activate ${skill.name}`, 'error'); } } + // Backwards compatibility wrapper + function activateSecondWind() { + activateUtilitySkill('second_wind'); + } + // Fetch player buffs from server async function fetchPlayerBuffs() { const token = localStorage.getItem('accessToken'); @@ -12478,21 +12522,98 @@ if (response.ok) { const buffs = await response.json(); + + // Reset all multipliers to base values + mpRegenMultiplier = 1.0; + exploreRadiusMultiplier = 1.0; + homebaseRadiusMultiplier = 1.0; + // Convert array to object keyed by buffType playerBuffs = {}; buffs.forEach(b => { playerBuffs[b.buffType] = b; - // Update MP regen multiplier if active - if (b.buffType === 'second_wind' && b.isActive) { - mpRegenMultiplier = b.effectValue; + // Apply active buff effects based on effectType + if (b.isActive) { + switch (b.effectType) { + case 'mp_regen_multiplier': + mpRegenMultiplier = b.effectValue; + break; + case 'explore_radius_multiplier': + exploreRadiusMultiplier = b.effectValue; + break; + case 'homebase_radius_multiplier': + homebaseRadiusMultiplier = b.effectValue; + break; + } } }); + + // Update fog of war if any radius multipliers changed + updateFogOfWar(); + updateGeocacheVisibility(); } } catch (err) { console.error('Error fetching player buffs:', err); } } + // Handle buff expiry by resetting multipliers + function handleBuffExpiry(effectType) { + switch (effectType) { + case 'mp_regen_multiplier': + mpRegenMultiplier = 1.0; + break; + case 'explore_radius_multiplier': + exploreRadiusMultiplier = 1.0; + updateFogOfWar(); + updateGeocacheVisibility(); + break; + case 'homebase_radius_multiplier': + homebaseRadiusMultiplier = 1.0; + updateFogOfWar(); + updateGeocacheVisibility(); + break; + } + } + + // Periodic buff timer update (runs every second when character sheet is open) + setInterval(() => { + const charSheetModal = document.getElementById('charSheetModal'); + if (charSheetModal && charSheetModal.style.display === 'flex') { + let needsUpdate = false; + + // Decrement expiresIn and cooldownEndsIn for all buffs + Object.keys(playerBuffs).forEach(key => { + const buff = playerBuffs[key]; + if (buff.expiresIn > 0) { + buff.expiresIn--; + needsUpdate = true; + } + if (buff.cooldownEndsIn > 0) { + buff.cooldownEndsIn--; + needsUpdate = true; + } + + // Check if buff just expired + if (buff.isActive && buff.expiresIn <= 0) { + buff.isActive = false; + // Reset corresponding multiplier + handleBuffExpiry(buff.effectType); + } + + // Check if cooldown just ended + if (buff.isOnCooldown && buff.cooldownEndsIn <= 0) { + buff.isOnCooldown = false; + needsUpdate = true; + } + }); + + if (needsUpdate) { + updateDailySkillsSection(); + } + } + }, 1000); + function hideCharacterSheet() { document.getElementById('charSheetModal').style.display = 'none'; }