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';
}