@ -2432,6 +2432,99 @@
0%, 100% { box-shadow: 0 0 0 0 rgba(243, 156, 18, 0.7); }
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); }
50% { box-shadow: 0 0 0 15px rgba(243, 156, 18, 0); }
}
}
.music-toggle-btn {
position: fixed;
bottom: 160px;
right: 15px;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 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;
}
.music-toggle-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.music-toggle-btn.muted {
background: linear-gradient(135deg, #7f8c8d 0%, #606c70 100%);
}
/* WASD Control Pad */
.wasd-controls {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 2100;
display: grid;
grid-template-columns: 50px 50px 50px;
grid-template-rows: 50px 50px;
gap: 4px;
opacity: 0.85;
}
.wasd-controls.hidden {
display: none;
}
.wasd-btn {
width: 50px;
height: 50px;
border-radius: 8px;
background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
border: 2px solid #4a6785;
color: #fff;
font-size: 18px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
transition: all 0.1s;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.wasd-btn:active {
transform: scale(0.95);
background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%);
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
}
.wasd-btn.w-btn {
grid-column: 2;
grid-row: 1;
}
.wasd-btn.a-btn {
grid-column: 1;
grid-row: 2;
}
.wasd-btn.s-btn {
grid-column: 2;
grid-row: 2;
}
.wasd-btn.d-btn {
grid-column: 3;
grid-row: 2;
}
.wasd-mode-indicator {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: #4fc3f7;
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
white-space: nowrap;
}
/* Home Base Marker */
/* Home Base Marker */
.home-base-marker {
.home-base-marker {
@ -2658,8 +2751,20 @@
<!-- Home Base Button -->
<!-- Home Base Button -->
< button id = "homeBaseBtn" class = "home-base-btn" style = "display: none;" onclick = "toggleHomeBaseSelection()" title = "Set Home Base" > 🏠< / button >
< button id = "homeBaseBtn" class = "home-base-btn" style = "display: none;" onclick = "toggleHomeBaseSelection()" title = "Set Home Base" > 🏠< / button >
<!-- Music Toggle Button -->
< button id = "musicToggleBtn" class = "music-toggle-btn" style = "display: none;" onclick = "toggleMusicMute()" title = "Toggle Music" > 🎵< / button >
<!-- WASD Control Pad -->
< div id = "wasdControls" class = "wasd-controls" >
< div id = "wasdModeIndicator" class = "wasd-mode-indicator" > TEST MODE< / div >
< button class = "wasd-btn w-btn" data-dir = "w" > ▲< / button >
< button class = "wasd-btn a-btn" data-dir = "a" > ◀< / button >
< button class = "wasd-btn s-btn" data-dir = "s" > ▼< / button >
< button class = "wasd-btn d-btn" data-dir = "d" > ▶< / button >
< / div >
<!-- Home Base Selection Hint -->
<!-- Home Base Selection Hint -->
< div id = "selectionHint" class = "selection-hint" style = "display: none;" > Tap on the map to set your home base< / div >
< div id = "selectionHint" class = "selection-hint" style = "display: none;" > Double-t ap on the map to set your home base< / div >
<!-- Death Overlay -->
<!-- Death Overlay -->
< div id = "deathOverlay" class = "death-overlay" style = "display: none;" >
< div id = "deathOverlay" class = "death-overlay" style = "display: none;" >
@ -3697,6 +3802,7 @@
spawnSettings.spawnInterval = settings.spawnInterval || 20000;
spawnSettings.spawnInterval = settings.spawnInterval || 20000;
spawnSettings.spawnChance = settings.spawnChance || 50;
spawnSettings.spawnChance = settings.spawnChance || 50;
spawnSettings.spawnDistance = settings.spawnDistance || 10;
spawnSettings.spawnDistance = settings.spawnDistance || 10;
spawnSettings.mpRegenDistance = settings.mpRegenDistance || 5;
console.log('Loaded spawn settings:', spawnSettings);
console.log('Loaded spawn settings:', spawnSettings);
}
}
} catch (err) {
} catch (err) {
@ -3860,16 +3966,35 @@
return Math.random() * 100 < hitChance ;
return Math.random() * 100 < hitChance ;
}
}
// Apply damage variance (±15% randomness)
function applyDamageVariance(baseDamage) {
const variance = 0.15; // 15% variance
const multiplier = 1 + (Math.random() * variance * 2 - variance); // 0.85 to 1.15
return Math.max(1, Math.floor(baseDamage * multiplier));
}
// Calculate damage with percentage-based defense reduction
// Formula: damage = rawDamage * (100 / (100 + DEF))
// This prevents DEF from completely nullifying attacks
function calculateDamage(rawDamage, defense) {
const reduction = 100 / (100 + defense);
const damage = Math.floor(rawDamage * reduction);
return Math.max(1, damage);
}
// Select a monster skill using weighted random
// Select a monster skill using weighted random
function selectMonsterSkill(monsterTypeId, monsterLevel) {
function selectMonsterSkill(monsterTypeId, monsterLevel) {
const skills = MONSTER_SKILLS[monsterTypeId] || [];
const skills = MONSTER_SKILLS[monsterTypeId] || [];
console.log('[DEBUG] selectMonsterSkill:', monsterTypeId, 'level:', monsterLevel, 'skills loaded:', skills.length);
// Filter by level requirement
// Filter by level requirement
const validSkills = skills.filter(s => monsterLevel >= s.minLevel);
const validSkills = skills.filter(s => monsterLevel >= s.minLevel);
console.log('[DEBUG] validSkills after level filter:', validSkills.length);
if (validSkills.length === 0) {
if (validSkills.length === 0) {
// Fallback to basic attack
// Fallback to basic attack
return SKILLS_DB['basic_attack'] || { id: 'basic_attack', name: 'Attack', basePower: 100, accuracy: 95 };
console.log('[DEBUG] Using fallback basic_attack, SKILLS_DB has it:', !!SKILLS_DB['basic_attack']);
return SKILLS_DB['basic_attack'] || { id: 'basic_attack', name: 'Attack', basePower: 100, accuracy: 95, type: 'damage', hitCount: 1 };
}
}
// Weighted random selection
// Weighted random selection
@ -3925,18 +4050,194 @@
let spawnSettings = {
let spawnSettings = {
spawnInterval: 20000, // Timer interval in ms
spawnInterval: 20000, // Timer interval in ms
spawnChance: 50, // Percent chance per interval
spawnChance: 50, // Percent chance per interval
spawnDistance: 10 // Meters player must move
spawnDistance: 10, // Meters player must move
mpRegenDistance: 5 // Meters per 1 MP regen
};
};
// MP regen tracking
let lastMpRegenLocation = null; // Track location for MP regen distance
let mpRegenAccumulator = 0; // Accumulated distance for MP regen
// Home Base state variables
// Home Base state variables
let homeBaseMarker = null; // Leaflet marker for home base
let homeBaseMarker = null; // Leaflet marker for home base
let homeBaseSelectionMode = false; // Whether we're in home base selection mode
let homeBaseSelectionMode = false; // Whether we're in home base selection mode
let xpLostOnDeath = 0; // Track XP lost for display
let xpLostOnDeath = 0; // Track XP lost for display
let lastHomeRegenTime = 0; // Track last HP/MP regen at home base
let lastHomeRegenTime = 0; // Track last HP/MP regen at home base
let wasAtHomeBase = false; // Track if player was at home base last check
const HOME_REGEN_INTERVAL = 3000; // Regen every 3 seconds when at home
const HOME_REGEN_INTERVAL = 3000; // Regen every 3 seconds when at home
const HOME_REGEN_PERCENT = 5; // Regen 5% of max HP/MP per tick
const HOME_REGEN_PERCENT = 5; // Regen 5% of max HP/MP per tick
const HOME_BASE_RADIUS = 20; // Meters - radius for home base effects
const HOME_BASE_RADIUS = 20; // Meters - radius for home base effects
// ==========================================
// MUSIC SYSTEM
// ==========================================
const gameMusic = {
overworld: new Audio('/mapgamemusic/over_world.mp3'),
battle: new Audio('/mapgamemusic/in_fight.mp3'),
victory: new Audio('/mapgamemusic/victory.mp3'),
death: new Audio('/mapgamemusic/he_ded.mp3'),
homebase: new Audio('/mapgamemusic/homebase.mp3'),
current: null,
currentTrack: null, // Track name for easy comparison
muted: localStorage.getItem('musicMuted') === 'true',
volume: parseFloat(localStorage.getItem('musicVolume') || '0.5'),
pausedTracks: {} // Store paused positions for resumable tracks
};
// Initialize music settings
function initMusic() {
// Set up looping for ambient tracks
gameMusic.overworld.loop = true;
gameMusic.battle.loop = true;
gameMusic.homebase.loop = true;
gameMusic.victory.loop = false;
gameMusic.death.loop = false;
// Set initial volume for all tracks
gameMusic.overworld.volume = gameMusic.volume;
gameMusic.battle.volume = gameMusic.volume;
gameMusic.victory.volume = gameMusic.volume;
gameMusic.death.volume = gameMusic.volume;
gameMusic.homebase.volume = gameMusic.volume;
// Preload all audio tracks and add error handling
const tracks = ['overworld', 'battle', 'victory', 'death', 'homebase'];
tracks.forEach(track => {
const audio = gameMusic[track];
audio.preload = 'auto';
audio.load();
audio.addEventListener('canplaythrough', () => {
console.log('Music track loaded:', track);
}, { once: true });
audio.addEventListener('error', (e) => {
console.error('Failed to load music track:', track, audio.error);
});
});
// When victory music ends, go back to appropriate music
gameMusic.victory.addEventListener('ended', () => {
if (!combatState & & playerStats & & !playerStats.isDead) {
// Check if at home base
const distToHome = getDistanceToHome();
if (distToHome !== null & & distToHome < = HOME_BASE_RADIUS) {
playMusic('homebase');
} else {
playMusic('overworld');
}
}
});
// When death music ends, stay silent (they need to get home)
gameMusic.death.addEventListener('ended', () => {
// Could loop or stay silent - staying silent for now
});
}
// Play a specific music track
function playMusic(track) {
if (gameMusic.muted) return;
const audio = gameMusic[track];
if (!audio) {
console.error('Music track not found:', track);
return;
}
// Don't restart if already playing this track
if (gameMusic.currentTrack === track & & !audio.paused) return;
// Pause current music (save position for overworld)
pauseCurrentMusic();
// Play new track
gameMusic.current = audio;
gameMusic.currentTrack = track;
// Resume from saved position for overworld, otherwise start fresh
if (track === 'overworld' & & gameMusic.pausedTracks.overworld !== undefined) {
audio.currentTime = gameMusic.pausedTracks.overworld;
delete gameMusic.pausedTracks.overworld;
} else if (track !== 'overworld') {
// Non-overworld tracks always start from beginning
audio.currentTime = 0;
}
// If overworld with no saved position, let it continue or start from 0
audio.play().then(() => {
console.log('Playing music:', track);
}).catch(err => {
console.log('Music autoplay blocked:', err.message);
});
}
// Pause current music (save position for resumable tracks)
function pauseCurrentMusic() {
if (gameMusic.current & & gameMusic.currentTrack) {
// Save position for overworld so we can resume later
if (gameMusic.currentTrack === 'overworld') {
gameMusic.pausedTracks.overworld = gameMusic.current.currentTime;
}
gameMusic.current.pause();
}
}
// Stop current music
function stopMusic() {
if (gameMusic.current) {
gameMusic.current.pause();
gameMusic.current.currentTime = 0;
}
gameMusic.currentTrack = null;
}
// Toggle mute
function toggleMusicMute() {
gameMusic.muted = !gameMusic.muted;
localStorage.setItem('musicMuted', gameMusic.muted);
if (gameMusic.muted) {
stopMusic();
} else {
// Resume appropriate music based on game state
if (playerStats & & playerStats.isDead) {
playMusic('death');
} else if (combatState & & combatState.inCombat) {
playMusic('battle');
} else {
// Check if at home base
const distToHome = getDistanceToHome();
if (distToHome !== null & & distToHome < = HOME_BASE_RADIUS) {
playMusic('homebase');
} else {
playMusic('overworld');
}
}
}
updateMusicButton();
}
// Set music volume
function setMusicVolume(vol) {
gameMusic.volume = Math.max(0, Math.min(1, vol));
localStorage.setItem('musicVolume', gameMusic.volume);
gameMusic.overworld.volume = gameMusic.volume;
gameMusic.battle.volume = gameMusic.volume;
gameMusic.victory.volume = gameMusic.volume;
gameMusic.death.volume = gameMusic.volume;
gameMusic.homebase.volume = gameMusic.volume;
}
// Update music button icon
function updateMusicButton() {
const btn = document.getElementById('musicToggleBtn');
if (btn) {
btn.innerHTML = gameMusic.muted ? '🔇' : '🎵';
btn.title = gameMusic.muted ? 'Unmute Music' : 'Mute Music';
}
}
// Find nearest monster to a location (for double-tap to battle on mobile)
// Find nearest monster to a location (for double-tap to battle on mobile)
function findNearestMonster(latlng, maxDistanceMeters = 50) {
function findNearestMonster(latlng, maxDistanceMeters = 50) {
if (monsterEntourage.length === 0) return null;
if (monsterEntourage.length === 0) return null;
@ -4136,6 +4437,12 @@
// Check for HP/MP regeneration at home base
// Check for HP/MP regeneration at home base
checkHomeBaseRegen();
checkHomeBaseRegen();
// Check for MP regeneration while walking
checkWalkingMpRegen(lat, lng);
// Check for monster clear at home base
checkHomeBaseMonsterClear();
// Update geocache visibility based on new location
// Update geocache visibility based on new location
if (navMode) {
if (navMode) {
updateGeocacheVisibility();
updateGeocacheVisibility();
@ -4373,6 +4680,116 @@
// Add keydown listener for GPS test mode
// Add keydown listener for GPS test mode
document.addEventListener('keydown', handleGpsTestKeydown);
document.addEventListener('keydown', handleGpsTestKeydown);
// WASD control pad button handlers
let wasdMoveInterval = null;
let wasdCurrentDir = null;
function initWasdControls() {
const wasdControls = document.getElementById('wasdControls');
const buttons = wasdControls.querySelectorAll('.wasd-btn');
// Move once in the specified direction
const doMove = (dir) => {
// Auto-enable GPS test mode if not already enabled
if (!gpsTestMode) {
// Initialize test position to current map center or user location
if (userLocation) {
testPosition = { lat: userLocation.lat, lng: userLocation.lng };
} else {
const center = map.getCenter();
testPosition = { lat: center.lat, lng: center.lng };
}
gpsTestMode = true;
// Stop real GPS
if (gpsWatchId !== null) {
navigator.geolocation.clearWatch(gpsWatchId);
gpsWatchId = null;
}
if (gpsBackupInterval) {
clearInterval(gpsBackupInterval);
gpsBackupInterval = null;
}
// Update admin toggle if it exists
const toggle = document.getElementById('gpsTestModeToggle');
if (toggle) toggle.checked = true;
updateStatus('Test mode enabled via controls', 'info');
}
// Move in the specified direction
switch (dir) {
case 'w':
testPosition.lat += GPS_TEST_STEP;
break;
case 's':
testPosition.lat -= GPS_TEST_STEP;
break;
case 'a':
testPosition.lng -= GPS_TEST_STEP;
break;
case 'd':
testPosition.lng += GPS_TEST_STEP;
break;
}
simulateGpsPosition();
};
// Start moving (on press/touch start)
const startMove = (e, dir) => {
e.preventDefault();
e.stopPropagation();
// Move immediately
doMove(dir);
// Start repeating after a short delay
wasdCurrentDir = dir;
if (wasdMoveInterval) clearInterval(wasdMoveInterval);
wasdMoveInterval = setInterval(() => {
if (wasdCurrentDir) {
doMove(wasdCurrentDir);
}
}, 100); // Move every 100ms while held
};
// Stop moving (on release)
const stopMove = () => {
wasdCurrentDir = null;
if (wasdMoveInterval) {
clearInterval(wasdMoveInterval);
wasdMoveInterval = null;
}
};
buttons.forEach(btn => {
const dir = btn.dataset.dir;
// Touch events
btn.addEventListener('touchstart', (e) => startMove(e, dir), { passive: false });
btn.addEventListener('touchend', stopMove, { passive: false });
btn.addEventListener('touchcancel', stopMove, { passive: false });
// Mouse events for desktop
btn.addEventListener('mousedown', (e) => startMove(e, dir));
btn.addEventListener('mouseup', stopMove);
btn.addEventListener('mouseleave', stopMove);
});
// Also stop if touch/mouse ends outside buttons
document.addEventListener('touchend', stopMove);
document.addEventListener('mouseup', stopMove);
}
// Initialize WASD controls after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWasdControls);
} else {
initWasdControls();
}
// Navigation functions
// Navigation functions
function setDestination(track, index) {
function setDestination(track, index) {
// Remove old pin if exists
// Remove old pin if exists
@ -7577,8 +7994,20 @@
// Direct touch event binding for mobile (Leaflet doesn't support touchstart through map.on)
// Direct touch event binding for mobile (Leaflet doesn't support touchstart through map.on)
const mapContainer = map.getContainer();
const mapContainer = map.getContainer();
// Variables for home base double-tap detection
let homeBaseTapTime = 0;
let homeBaseTapLocation = null;
// Fix for Chrome and PWA - use native addEventListener with passive: false
// Fix for Chrome and PWA - use native addEventListener with passive: false
mapContainer.addEventListener('touchstart', function(e) {
mapContainer.addEventListener('touchstart', function(e) {
// Home base selection mode takes priority - capture touch for double-tap detection
if (homeBaseSelectionMode & & e.touches.length === 1) {
e.preventDefault();
e.stopPropagation();
touchStartTime = Date.now();
return;
}
if (navMode & & e.touches.length === 1) {
if (navMode & & e.touches.length === 1) {
// Check if touch target is a geocache marker - let those through to Leaflet
// Check if touch target is a geocache marker - let those through to Leaflet
if (e.target.closest('.geocache-marker')) {
if (e.target.closest('.geocache-marker')) {
@ -7605,6 +8034,51 @@
}, { passive: false, capture: true });
}, { passive: false, capture: true });
mapContainer.addEventListener('touchend', function(e) {
mapContainer.addEventListener('touchend', function(e) {
// Home base selection mode - handle double-tap to place home base
if (homeBaseSelectionMode) {
e.preventDefault();
e.stopPropagation();
const now = Date.now();
const timeSinceLastTap = now - homeBaseTapTime;
// Get current tap location
let currentTapLocation = null;
if (e.changedTouches & & e.changedTouches.length > 0) {
const touch = e.changedTouches[0];
currentTapLocation = { x: touch.clientX, y: touch.clientY };
}
// Check for double-tap (two taps within 400ms at roughly same location)
if (timeSinceLastTap < 400 & & homeBaseTapLocation & & currentTapLocation ) {
const dx = currentTapLocation.x - homeBaseTapLocation.x;
const dy = currentTapLocation.y - homeBaseTapLocation.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Only trigger if taps are within 50 pixels of each other
if (distance < 50 ) {
// Convert touch to latlng and set home base
const rect = mapContainer.getBoundingClientRect();
const x = currentTapLocation.x - rect.left;
const y = currentTapLocation.y - rect.top;
const containerPoint = L.point(x, y);
const latlng = map.containerPointToLatLng(containerPoint);
setHomeBase(latlng.lat, latlng.lng);
// Reset double-tap detection
homeBaseTapTime = 0;
homeBaseTapLocation = null;
return;
}
}
// Store this tap for double-tap detection
homeBaseTapTime = now;
homeBaseTapLocation = currentTapLocation;
return;
}
if (navMode) {
if (navMode) {
const now = Date.now();
const now = Date.now();
const timeSinceLastTap = now - lastTapTime;
const timeSinceLastTap = now - lastTapTime;
@ -7744,6 +8218,14 @@
// This prevents the 50/50 race condition between handlers
// This prevents the 50/50 race condition between handlers
if ('ontouchstart' in window) return;
if ('ontouchstart' in window) return;
// Home base selection mode - double-click to place
if (homeBaseSelectionMode) {
L.DomEvent.stopPropagation(e);
L.DomEvent.preventDefault(e);
setHomeBase(e.latlng.lat, e.latlng.lng);
return;
}
if (navMode) {
if (navMode) {
L.DomEvent.stopPropagation(e);
L.DomEvent.stopPropagation(e);
L.DomEvent.preventDefault(e);
L.DomEvent.preventDefault(e);
@ -10179,6 +10661,14 @@
// Show/hide home base button
// Show/hide home base button
document.getElementById('homeBaseBtn').style.display = playerStats ? 'flex' : 'none';
document.getElementById('homeBaseBtn').style.display = playerStats ? 'flex' : 'none';
// Show/hide music button and update state
const musicBtn = document.getElementById('musicToggleBtn');
if (musicBtn) {
musicBtn.style.display = playerStats ? 'flex' : 'none';
musicBtn.classList.toggle('muted', gameMusic.muted);
updateMusicButton();
}
}
}
// ==========================================
// ==========================================
@ -10526,6 +11016,122 @@
}, 500);
}, 500);
}
}
// Check for MP regeneration while walking
function checkWalkingMpRegen(lat, lng) {
// Skip if dead, in combat, or no player stats
if (!playerStats || playerStats.isDead) return;
if (combatState & & combatState.inCombat) return;
// Skip if already at max MP
if (playerStats.mp >= playerStats.maxMp) return;
// Get the regen distance setting (default 5 meters per 1 MP)
const regenDistance = spawnSettings.mpRegenDistance || 5;
if (regenDistance < = 0) return; // Disabled if 0 or negative
// Initialize last location if not set
if (!lastMpRegenLocation) {
lastMpRegenLocation = { lat, lng };
return;
}
// Calculate distance walked since last check (in meters)
const metersPerDegLat = 111320;
const metersPerDegLng = 111320 * Math.cos(lat * Math.PI / 180);
const dx = (lng - lastMpRegenLocation.lng) * metersPerDegLng;
const dy = (lat - lastMpRegenLocation.lat) * metersPerDegLat;
const distanceWalked = Math.sqrt(dx * dx + dy * dy);
// Update last location
lastMpRegenLocation = { lat, lng };
// Skip very small movements (GPS jitter) or very large jumps (teleport/error)
if (distanceWalked < 0.5 | | distanceWalked > 50) return;
// Accumulate distance
mpRegenAccumulator += distanceWalked;
// Check if we've walked enough for MP regen
if (mpRegenAccumulator >= regenDistance) {
const mpToRegen = Math.floor(mpRegenAccumulator / regenDistance);
mpRegenAccumulator = mpRegenAccumulator % regenDistance; // Keep remainder
const oldMp = playerStats.mp;
playerStats.mp = Math.min(playerStats.maxMp, playerStats.mp + mpToRegen);
if (playerStats.mp > oldMp) {
updateRpgHud();
savePlayerStats();
// Show subtle MP regen indicator
showWalkingMpRegenEffect();
}
}
}
// Show a subtle visual effect for walking MP regen
function showWalkingMpRegenEffect() {
const hud = document.getElementById('rpgHud');
if (!hud) return;
// Add a brief blue glow for MP regen
hud.style.boxShadow = '0 0 15px rgba(33, 150, 243, 0.6)';
setTimeout(() => {
hud.style.boxShadow = '';
}, 300);
}
// Check if at home base and clear monsters if entering
function checkHomeBaseMonsterClear() {
const distance = getDistanceToHome();
const isAtHome = distance !== null & & distance < = HOME_BASE_RADIUS;
// Check if player just entered home base
if (isAtHome & & !wasAtHomeBase) {
clearAllMonsters();
// Play homebase music (unless dead or in combat)
if (playerStats & & !playerStats.isDead & & !combatState) {
playMusic('homebase');
}
}
// Check if player just left home base
if (!isAtHome & & wasAtHomeBase) {
// Switch to overworld music (unless dead or in combat)
if (playerStats & & !playerStats.isDead & & !combatState) {
playMusic('overworld');
}
}
// Update tracking state
wasAtHomeBase = isAtHome;
}
// Clear all monsters from the map (used when entering home base)
function clearAllMonsters() {
if (monsterEntourage.length === 0) return;
// Remove all monster markers from the map
for (const monster of monsterEntourage) {
if (monster.marker) {
monster.marker.remove();
}
}
// Clear the array
const count = monsterEntourage.length;
monsterEntourage.length = 0;
// Show notification
if (count > 0) {
showToast(`🏠 Entered home base - ${count} monster${count > 1 ? 's' : ''} fled!`, 'info');
}
// Update HUD
updateRpgHud();
}
// Handle player death
// Handle player death
async function handlePlayerDeath() {
async function handlePlayerDeath() {
const token = localStorage.getItem('accessToken');
const token = localStorage.getItem('accessToken');
@ -10557,6 +11163,9 @@
document.getElementById('xpLostText').textContent = `-${xpLostOnDeath} XP`;
document.getElementById('xpLostText').textContent = `-${xpLostOnDeath} XP`;
document.getElementById('deathOverlay').style.display = 'flex';
document.getElementById('deathOverlay').style.display = 'flex';
// Play death music
playMusic('death');
// Update HUD
// Update HUD
updateRpgHud();
updateRpgHud();
@ -10590,6 +11199,9 @@
// Hide death overlay
// Hide death overlay
document.getElementById('deathOverlay').style.display = 'none';
document.getElementById('deathOverlay').style.display = 'none';
// Play homebase music (player respawns at home)
playMusic('homebase');
// Update HUD
// Update HUD
updateRpgHud();
updateRpgHud();
savePlayerStats();
savePlayerStats();
@ -10762,6 +11374,10 @@
if (monsterEntourage.length >= getMaxMonsters()) return;
if (monsterEntourage.length >= getMaxMonsters()) return;
if (!monsterTypesLoaded || Object.keys(MONSTER_TYPES).length === 0) return;
if (!monsterTypesLoaded || Object.keys(MONSTER_TYPES).length === 0) return;
// Don't spawn monsters at home base
const distanceToHome = getDistanceToHome();
if (distanceToHome !== null & & distanceToHome < = HOME_BASE_RADIUS) return;
// Movement-based spawning: first monster can spawn standing still,
// Movement-based spawning: first monster can spawn standing still,
// but subsequent monsters require player to move the configured distance
// but subsequent monsters require player to move the configured distance
if (monsterEntourage.length > 0 & & lastSpawnLocation) {
if (monsterEntourage.length > 0 & & lastSpawnLocation) {
@ -11009,6 +11625,9 @@
const overlay = document.getElementById('combatOverlay');
const overlay = document.getElementById('combatOverlay');
overlay.style.display = 'flex';
overlay.style.display = 'flex';
// Play battle music
playMusic('battle');
// Render monster list
// Render monster list
renderMonsterList();
renderMonsterList();
@ -11220,8 +11839,12 @@
// Update skill button states
// Update skill button states
document.querySelectorAll('.skill-btn').forEach(btn => {
document.querySelectorAll('.skill-btn').forEach(btn => {
const skillId = btn.dataset.skillId;
const skillId = btn.dataset.skillId;
const skill = SKILLS[skillId];
btn.disabled = combatState.player.mp < skill.mpCost | | combatState . turn ! = = ' player ' ;
const skill = SKILLS_DB[skillId] || SKILLS[skillId];
if (!skill) {
btn.disabled = true;
return;
}
btn.disabled = combatState.player.mp < (skill.mpCost || 0) || combatState.turn !== 'player';
});
});
// Disable flee button during monster turns
// Disable flee button during monster turns
@ -11291,12 +11914,17 @@
// Execute a player skill
// Execute a player skill
function executePlayerSkill(skillId) {
function executePlayerSkill(skillId) {
if (!combatState || combatState.turn !== 'player') return;
console.log('[DEBUG] executePlayerSkill called with:', skillId);
if (!combatState || combatState.turn !== 'player') {
console.log('[DEBUG] Early return - combatState:', !!combatState, 'turn:', combatState?.turn);
return;
}
// Get skill from DB first, then fall back to hardcoded SKILLS
// Get skill from DB first, then fall back to hardcoded SKILLS
const dbSkill = SKILLS_DB[skillId];
const dbSkill = SKILLS_DB[skillId];
const hardcodedSkill = SKILLS[skillId];
const hardcodedSkill = SKILLS[skillId];
const skill = dbSkill || hardcodedSkill;
const skill = dbSkill || hardcodedSkill;
console.log('[DEBUG] Skill found:', skill?.name, 'type:', skill?.type);
if (!skill) {
if (!skill) {
addCombatLog(`Unknown skill: ${skillId}`);
addCombatLog(`Unknown skill: ${skillId}`);
@ -11395,7 +12023,8 @@
}
}
for (let hit = 0; hit < hitCount ; hit + + ) {
for (let hit = 0; hit < hitCount ; hit + + ) {
const damage = Math.max(1, rawDamage - effectiveMonsterDef);
const baseDamage = calculateDamage(rawDamage, effectiveMonsterDef);
const damage = applyDamageVariance(baseDamage);
totalDamage += damage;
totalDamage += damage;
currentTarget.hp -= damage;
currentTarget.hp -= damage;
}
}
@ -11414,9 +12043,22 @@
// Check if this monster died
// Check if this monster died
if (currentTarget.hp < = 0) {
if (currentTarget.hp < = 0) {
monstersKilled++;
monstersKilled++;
// Award XP immediately for this kill
const xpReward = (currentTarget.data?.xpReward || 10) * currentTarget.level;
playerStats.xp += xpReward;
combatState.player.xpGained = (combatState.player.xpGained || 0) + xpReward;
if (targets.length === 1) {
if (targets.length === 1) {
addCombatLog(`💀 ${currentTarget.data.name} was defeated!`, 'victory');
addCombatLog(`💀 ${currentTarget.data.name} was defeated! +${xpReward} XP `, 'victory');
}
}
// Remove this monster from entourage immediately
removeMonster(currentTarget.id);
// Check for level up
checkLevelUp();
savePlayerStats();
updateRpgHud();
}
}
}
}
@ -11427,7 +12069,8 @@
} else {
} else {
addCombatLog(`🌟 ${displayName} hits ${monstersHit} enemies for ${grandTotalDamage} total damage!`, 'damage');
addCombatLog(`🌟 ${displayName} hits ${monstersHit} enemies for ${grandTotalDamage} total damage!`, 'damage');
if (monstersKilled > 0) {
if (monstersKilled > 0) {
addCombatLog(`💀 ${monstersKilled} enemy${monstersKilled > 1 ? 'ies' : ''} defeated!`, 'victory');
const totalXpGained = combatState.player.xpGained || 0;
addCombatLog(`💀 ${monstersKilled} enemy${monstersKilled > 1 ? 'ies' : ''} defeated! +${totalXpGained} XP`, 'victory');
}
}
}
}
}
}
@ -11463,7 +12106,9 @@
// Check if ALL monsters are defeated
// Check if ALL monsters are defeated
const livingMonsters = combatState.monsters.filter(m => m.hp > 0);
const livingMonsters = combatState.monsters.filter(m => m.hp > 0);
console.log('[DEBUG] Victory check - livingMonsters:', livingMonsters.length, 'monsters:', combatState.monsters.map(m => ({type: m.type, hp: m.hp})));
if (livingMonsters.length === 0) {
if (livingMonsters.length === 0) {
console.log('[DEBUG] All monsters defeated, calling handleCombatVictory');
handleCombatVictory();
handleCombatVictory();
return;
return;
}
}
@ -11473,9 +12118,11 @@
// End player turn and start monster turns
// End player turn and start monster turns
function endPlayerTurn() {
function endPlayerTurn() {
console.log('[DEBUG] endPlayerTurn called');
combatState.turn = 'monsters';
combatState.turn = 'monsters';
combatState.currentMonsterTurn = 0;
combatState.currentMonsterTurn = 0;
updateCombatUI();
updateCombatUI();
console.log('[DEBUG] Scheduling executeMonsterTurns in 800ms');
setTimeout(executeMonsterTurns, 800);
setTimeout(executeMonsterTurns, 800);
}
}
@ -11498,8 +12145,10 @@
// Execute all monster turns sequentially
// Execute all monster turns sequentially
function executeMonsterTurns() {
function executeMonsterTurns() {
console.log('[DEBUG] executeMonsterTurns called, combatState:', !!combatState);
if (!combatState) return;
if (!combatState) return;
console.log('[DEBUG] currentMonsterTurn:', combatState.currentMonsterTurn, 'total:', combatState.monsters.length);
// Find next living monster starting from currentMonsterTurn
// Find next living monster starting from currentMonsterTurn
while (combatState.currentMonsterTurn < combatState.monsters.length ) {
while (combatState.currentMonsterTurn < combatState.monsters.length ) {
const monster = combatState.monsters[combatState.currentMonsterTurn];
const monster = combatState.monsters[combatState.currentMonsterTurn];
@ -11526,9 +12175,11 @@
// Execute one monster's attack
// Execute one monster's attack
function executeOneMonsterAttack(monsterIndex) {
function executeOneMonsterAttack(monsterIndex) {
console.log('[DEBUG] executeOneMonsterAttack called, monsterIndex:', monsterIndex);
if (!combatState) return;
if (!combatState) return;
const monster = combatState.monsters[monsterIndex];
const monster = combatState.monsters[monsterIndex];
console.log('[DEBUG] Monster:', monster?.type, 'HP:', monster?.hp);
combatState.currentMonsterTurn = monsterIndex;
combatState.currentMonsterTurn = monsterIndex;
updateCombatUI();
updateCombatUI();
@ -11544,6 +12195,7 @@
// Select a skill using weighted random (or basic attack if none)
// Select a skill using weighted random (or basic attack if none)
const selectedSkill = selectMonsterSkill(monster.type, monster.level);
const selectedSkill = selectMonsterSkill(monster.type, monster.level);
console.log('[DEBUG] Selected skill:', selectedSkill?.id, selectedSkill?.name, 'type:', selectedSkill?.type);
// Calculate hit chance
// Calculate hit chance
const skillAccuracy = selectedSkill.accuracy || 85;
const skillAccuracy = selectedSkill.accuracy || 85;
@ -11622,7 +12274,8 @@
// But we should factor in monster's attack buff if it has one
// But we should factor in monster's attack buff if it has one
for (let hit = 0; hit < hitCount ; hit + + ) {
for (let hit = 0; hit < hitCount ; hit + + ) {
const damage = Math.max(1, rawDamage - effectiveDef);
const baseDamage = calculateDamage(rawDamage, effectiveDef);
const damage = applyDamageVariance(baseDamage);
totalDamage += damage;
totalDamage += damage;
combatState.player.hp -= damage;
combatState.player.hp -= damage;
}
}
@ -11654,31 +12307,32 @@
// Handle combat victory
// Handle combat victory
function handleCombatVictory() {
function handleCombatVictory() {
// Calculate total XP from all defeated monsters
let totalXp = 0;
const monsterIds = [];
combatState.monsters.forEach(monster => {
totalXp += monster.data.xpReward * monster.level;
monsterIds.push(monster.id);
});
console.log('[DEBUG] handleCombatVictory called');
const monsterCount = combatState.monsters.length;
addCombatLog(`Victory! Defeated ${monsterCount} ${monsterCount === 1 ? 'enemy' : 'enemies'}! Gained ${totalXp} XP!`, 'victory');
// Play victory music
playMusic('victory');
// Remove all monsters from entourage
monsterIds.forEach(id => removeMonster(id));
try {
// XP was already awarded per-kill, just show total
const totalXp = combatState.player.xpGained || 0;
const monsterCount = combatState.monsters.length;
addCombatLog(`Victory! Defeated ${monsterCount} ${monsterCount === 1 ? 'enemy' : 'enemies'}!`, 'victory');
// Update player stats
playerStats.hp = combatState.player.hp;
playerStats.mp = combatState.player.mp;
playerStats.xp += totalXp;
// Monsters already removed per-kill, just update player HP/MP
playerStats.hp = combatState.player.hp;
playerStats.mp = combatState.player.mp;
// Check for level up
checkLevelUp();
// Check for level up
checkLevelUp();
savePlayerStats();
updateRpgHud();
savePlayerStats();
updateRpgHud();
} catch (err) {
console.error('[DEBUG] Error in handleCombatVictory:', err);
}
// Always close combat UI, even if there were errors above
console.log('[DEBUG] Scheduling closeCombatUI in 2500ms');
setTimeout(closeCombatUI, 2500);
setTimeout(closeCombatUI, 2500);
}
}
@ -11725,8 +12379,19 @@
// Close combat UI
// Close combat UI
function closeCombatUI() {
function closeCombatUI() {
console.log('[DEBUG] closeCombatUI called');
document.getElementById('combatOverlay').style.display = 'none';
document.getElementById('combatOverlay').style.display = 'none';
combatState = null;
combatState = null;
// If victory music isn't playing, switch to appropriate ambient music
if (gameMusic.currentTrack !== 'victory' || gameMusic.victory.paused) {
const distToHome = getDistanceToHome();
if (distToHome !== null & & distToHome < = HOME_BASE_RADIUS) {
playMusic('homebase');
} else {
playMusic('overworld');
}
}
}
}
// Check for level up
// Check for level up
@ -11770,6 +12435,30 @@
loadCurrentUser();
loadCurrentUser();
});
});
// Initialize music system
initMusic();
// Start appropriate music on first user interaction (required due to autoplay restrictions)
let musicStarted = false;
function startMusicOnInteraction() {
if (!musicStarted & & playerStats & & !gameMusic.muted) {
musicStarted = true;
// Play appropriate music based on game state
if (playerStats.isDead) {
playMusic('death');
} else {
const distToHome = getDistanceToHome();
if (distToHome !== null & & distToHome < = HOME_BASE_RADIUS) {
playMusic('homebase');
} else {
playMusic('overworld');
}
}
}
}
document.addEventListener('click', startMusicOnInteraction, { once: false });
document.addEventListener('touchstart', startMusicOnInteraction, { once: false });
// Show auth modal if not logged in (guest mode available)
// Show auth modal if not logged in (guest mode available)
if (!localStorage.getItem('accessToken') & & !sessionStorage.getItem('guestMode')) {
if (!localStorage.getItem('accessToken') & & !sessionStorage.getItem('guestMode')) {
setTimeout(() => {
setTimeout(() => {