Browse Source

Add utility skill effects for fog of war modification

- Add exploreRadiusMultiplier and homebaseRadiusMultiplier state variables
- Apply multipliers in updateFogOfWar() and isInRevealedArea()
- Generalize Daily Skills section to display all unlocked utility skills
- Create generic activateUtilitySkill() function with effect type handling
- Add explore_radius_multiplier and homebase_radius_multiplier effect types
- Add fog effect options to admin panel utility skill dropdown
- Add periodic buff timer to update countdowns and handle expiry

New effect types for utility skills:
- explore_radius_multiplier: Multiplies player's 50m explore radius
- homebase_radius_multiplier: Multiplies homebase's 800m reveal radius

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
master
HikeMap User 4 weeks ago
parent
commit
0f117ea4cf
  1. 6
      admin.html
  2. 189
      index.html

6
admin.html

@ -1523,6 +1523,8 @@
<option value="def_boost_flat">DEF Boost (Flat)</option>
<option value="def_boost_percent">DEF Boost (%)</option>
<option value="xp_multiplier">XP Multiplier</option>
<option value="explore_radius_multiplier">Explore Radius Multiplier</option>
<option value="homebase_radius_multiplier">Homebase Radius Multiplier</option>
</select>
</div>
<div class="form-group">
@ -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 => {

189
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,17 +12373,23 @@
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 = '<div class="no-daily-skills">No daily skills unlocked yet</div>';
return;
}
// Get Second Wind buff status
const buff = playerBuffs['second_wind'];
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';
@ -12398,74 +12412,104 @@
}
}
const secondWindIconHtml = renderSkillIcon('second_wind', 'class', playerStats?.class, 24);
container.innerHTML = `
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">${secondWindIconHtml}</span>
<span class="skill-icon">${iconHtml}</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-name">${displayName}</div>
<div class="skill-desc">${displayDesc}</div>
<div class="skill-status ${statusClass}">${statusText}</div>
</div>
<button class="daily-skill-btn ${buttonClass}"
onclick="activateSecondWind()"
onclick="activateUtilitySkill('${skillId}')"
${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');
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
// 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('Second Wind activated! MP regen doubled for 1 hour.', 'success');
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) {
// 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';
}

Loading…
Cancel
Save