@ -4267,8 +4267,9 @@
const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng);
const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng);
const centerPoint = map.latLngToContainerPoint(homeLatLng);
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 edgePoint = map.latLngToContainerPoint(edgeLatLng);
const radiusPixels = Math.abs(edgePoint.x - centerPoint.x);
const radiusPixels = Math.abs(edgePoint.x - centerPoint.x);
@ -4297,16 +4298,19 @@
const playerLatLng = L.latLng(userLocation.lat, userLocation.lng);
const playerLatLng = L.latLng(userLocation.lat, userLocation.lng);
// Check if player is outside homebase radius (or no homebase)
// Check if player is outside homebase radius (or no homebase)
// Use effective homebase radius with multiplier
const effectiveHomebaseRadius = playerRevealRadius * homebaseRadiusMultiplier;
let isOutsideHomebase = true;
let isOutsideHomebase = true;
if (playerStats & & playerStats.homeBaseLat != null & & playerStats.homeBaseLng != null) {
if (playerStats & & playerStats.homeBaseLat != null & & playerStats.homeBaseLng != null) {
const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng);
const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng);
const distToHome = playerLatLng.distanceTo(homeLatLng);
const distToHome = playerLatLng.distanceTo(homeLatLng);
isOutsideHomebase = distToHome > playerReveal Radius;
isOutsideHomebase = distToHome > effectiveHomebase Radius;
}
}
if (isOutsideHomebase) {
if (isOutsideHomebase) {
const playerPoint = map.latLngToContainerPoint(playerLatLng);
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 playerEdgePoint = map.latLngToContainerPoint(playerEdge);
const playerRadiusPixels = Math.abs(playerEdgePoint.x - playerPoint.x);
const playerRadiusPixels = Math.abs(playerEdgePoint.x - playerPoint.x);
@ -4334,18 +4338,20 @@
function isInRevealedArea(lat, lng) {
function isInRevealedArea(lat, lng) {
const checkPoint = L.latLng(lat, lng);
const checkPoint = L.latLng(lat, lng);
// Check homebase radius
// Check homebase radius (with multiplier)
if (playerStats & & playerStats.homeBaseLat != null & & playerStats.homeBaseLng != null) {
if (playerStats & & playerStats.homeBaseLat != null & & playerStats.homeBaseLng != null) {
const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng);
const homeLatLng = L.latLng(playerStats.homeBaseLat, playerStats.homeBaseLng);
if (homeLatLng.distanceTo(checkPoint) < = playerRevealRadius) {
const effectiveHomebaseRadius = playerRevealRadius * homebaseRadiusMultiplier;
if (homeLatLng.distanceTo(checkPoint) < = effectiveHomebaseRadius) {
return true;
return true;
}
}
}
}
// Check player's explore radius
// Check player's explore radius (with multiplier)
if (userLocation) {
if (userLocation) {
const playerLatLng = L.latLng(userLocation.lat, userLocation.lng);
const playerLatLng = L.latLng(userLocation.lat, userLocation.lng);
if (playerLatLng.distanceTo(checkPoint) < = playerExploreRadius) {
const effectiveExploreRadius = playerExploreRadius * exploreRadiusMultiplier;
if (playerLatLng.distanceTo(checkPoint) < = effectiveExploreRadius) {
return true;
return true;
}
}
}
}
@ -5181,6 +5187,8 @@
// Player buffs state (loaded from server)
// Player buffs state (loaded from server)
let playerBuffs = {}; // Buff status keyed by buffType
let playerBuffs = {}; // Buff status keyed by buffType
let mpRegenMultiplier = 1.0; // Current MP regen multiplier
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
// MUSIC SYSTEM
@ -12365,107 +12373,143 @@
const container = document.getElementById('charSheetDailySkills');
const container = document.getElementById('charSheetDailySkills');
if (!container) return;
if (!container) return;
// Check if player has Second Wind unlocked
const unlockedSkills = playerStats?.unlockedSkills || ['basic_attack'];
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 = '< div class = "no-daily-skills" > No daily skills unlocked yet< / div > ';
container.innerHTML = '< div class = "no-daily-skills" > No daily skills unlocked yet< / div > ';
return;
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 = `
< div class = "daily-skill" >
< span class = "skill-icon" > ${secondWindIconHtml}< / span >
< div class = "skill-info" >
< div class = "skill-name" > Second Wind< / div >
< div class = "skill-desc" > Double MP regen while walking for 1 hour< / div >
< div class = "skill-status ${statusClass}" > ${statusText}< / div >
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 `
< div class = "daily-skill" >
< span class = "skill-icon" > ${iconHtml}< / span >
< div class = "skill-info" >
< div class = "skill-name" > ${displayName}< / div >
< div class = "skill-desc" > ${displayDesc}< / div >
< div class = "skill-status ${statusClass}" > ${statusText}< / div >
< / div >
< button class = "daily-skill-btn ${buttonClass}"
onclick="activateUtilitySkill('${skillId}')"
${buttonDisabled ? 'disabled' : ''}>
${buttonText}
< / button >
< / div >
< / div >
< button class = "daily-skill-btn ${buttonClass}"
onclick="activateSecondWind()"
${buttonDisabled ? 'disabled' : ''}>
${buttonText}
< / button >
< / div >
`;
`;
}).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');
const token = localStorage.getItem('accessToken');
if (!token) {
if (!token) {
console.error('No access token for Second Wind activation');
showNotification('Please log in to use skills', 'error');
showNotification('Please log in to use skills', 'error');
return;
return;
}
}
const skill = SKILLS_DB[skillId] || SKILLS[skillId];
if (!skill) {
showNotification('Unknown skill', 'error');
return;
}
try {
try {
console.log('Sending buff activation request...');
const response = await fetch('/api/user/buffs/activate', {
const response = await fetch('/api/user/buffs/activate', {
method: 'POST',
method: 'POST',
headers: {
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
'Authorization': `Bearer ${token}`
},
},
body: JSON.stringify({ buffType: 'second_wind' })
body: JSON.stringify({ buffType: skillId })
});
});
if (response.ok) {
if (response.ok) {
const data = await response.json();
const data = await response.json();
// Update local buff state
// Update local buff state
playerBuffs['second_wind' ] = {
playerBuffs[skillId ] = {
isActive: true,
isActive: true,
isOnCooldown: true,
isOnCooldown: true,
expiresIn: data.expiresIn,
expiresIn: data.expiresIn,
cooldownEndsIn: data.cooldownEndsIn,
cooldownEndsIn: data.cooldownEndsIn,
effectType: data.effectType,
effectValue: data.effectValue
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();
updateDailySkillsSection();
} else {
} else {
const error = await response.json();
const error = await response.json();
if (error.cooldownEndsIn) {
if (error.cooldownEndsIn) {
updateStatus(`Second Wind on cooldown for ${formatTimeRemaining(error.cooldownEndsIn)}`, 'error');
updateStatus(`${skill.name} on cooldown for ${formatTimeRemaining(error.cooldownEndsIn)}`, 'error');
} else {
} else {
updateStatus(error.error || 'Failed to activate Second Wind', 'error');
updateStatus(error.error || `Failed to activate ${skill.name}` , 'error');
}
}
}
}
} catch (err) {
} 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
// Fetch player buffs from server
async function fetchPlayerBuffs() {
async function fetchPlayerBuffs() {
const token = localStorage.getItem('accessToken');
const token = localStorage.getItem('accessToken');
@ -12478,21 +12522,98 @@
if (response.ok) {
if (response.ok) {
const buffs = await response.json();
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
// Convert array to object keyed by buffType
playerBuffs = {};
playerBuffs = {};
buffs.forEach(b => {
buffs.forEach(b => {
playerBuffs[b.buffType] = 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) {
} catch (err) {
console.error('Error fetching player buffs:', 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() {
function hideCharacterSheet() {
document.getElementById('charSheetModal').style.display = 'none';
document.getElementById('charSheetModal').style.display = 'none';
}
}