diff --git a/index.html b/index.html index 4a3cd5a..800fb64 100644 --- a/index.html +++ b/index.html @@ -2519,6 +2519,17 @@ font-size: 10px; margin-top: 2px; } + .monster-entry-mp { + margin-top: 2px; + } + .monster-entry-mp .mp-bar { + height: 8px; + } + .monster-entry-mp .stat-text { + font-size: 9px; + margin-top: 1px; + color: #4ecdc4; + } .turn-indicator { text-align: center; padding: 8px; @@ -3903,6 +3914,7 @@ let gpsAccuracyCircle = null; let gpsFirstFix = true; let currentHeading = null; + let userLocation = null; // Last known GPS position {lat, lng, accuracy} // GPS test mode (admin only) let gpsTestMode = false; @@ -4102,13 +4114,19 @@ baseHp: t.baseHp, baseAtk: t.baseAtk, baseDef: t.baseDef, + baseMp: t.baseMp || 20, xpReward: t.xpReward, accuracy: t.accuracy || 85, dodge: t.dodge || 5, minLevel: t.minLevel || 1, maxLevel: t.maxLevel || 99, spawnWeight: t.spawnWeight || 100, - levelScale: t.levelScale + levelScale: { + hp: t.levelScale?.hp || 10, + atk: t.levelScale?.atk || 2, + def: t.levelScale?.def || 1, + mp: t.levelScale?.mp || 5 + } }; MONSTER_DIALOGUES[t.id] = t.dialogues; }); @@ -4316,18 +4334,18 @@ } // Select a monster skill using weighted random - function selectMonsterSkill(monsterTypeId, monsterLevel) { + function selectMonsterSkill(monsterTypeId, monsterLevel, monsterMp = 999) { const skills = MONSTER_SKILLS[monsterTypeId] || []; - console.log('[DEBUG] selectMonsterSkill:', monsterTypeId, 'level:', monsterLevel, 'skills loaded:', skills.length); + console.log('[DEBUG] selectMonsterSkill:', monsterTypeId, 'level:', monsterLevel, 'MP:', monsterMp, 'skills loaded:', skills.length); - // Filter by level requirement - const validSkills = skills.filter(s => monsterLevel >= s.minLevel); - console.log('[DEBUG] validSkills after level filter:', validSkills.length); + // Filter by level requirement AND MP cost + const validSkills = skills.filter(s => monsterLevel >= s.minLevel && (s.mpCost || 0) <= monsterMp); + console.log('[DEBUG] validSkills after level+MP filter:', validSkills.length); if (validSkills.length === 0) { // Fallback to basic attack 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 }; + return SKILLS_DB['basic_attack'] || { id: 'basic_attack', name: 'Attack', basePower: 100, accuracy: 95, type: 'damage', hitCount: 1, mpCost: 0 }; } // Weighted random selection @@ -4344,7 +4362,8 @@ accuracy: skill.accuracy, hitCount: skill.hitCount || 1, statusEffect: skill.statusEffect, - type: skill.type + type: skill.type, + mpCost: skill.mpCost || 0 }; } } @@ -4358,7 +4377,8 @@ accuracy: lastSkill.accuracy, hitCount: lastSkill.hitCount || 1, statusEffect: lastSkill.statusEffect, - type: lastSkill.type + type: lastSkill.type, + mpCost: lastSkill.mpCost || 0 }; } @@ -4712,6 +4732,11 @@ gpsAccuracyCircle = null; } + // Sync test position to last known GPS location for seamless manual mode + if (userLocation) { + testPosition = { lat: userLocation.lat, lng: userLocation.lng }; + } + gpsFirstFix = true; btn.textContent = 'Show My Location'; btn.classList.remove('active'); @@ -4723,6 +4748,15 @@ return; } + // Disable test mode when starting real GPS + if (gpsTestMode) { + gpsTestMode = false; + const infoDiv = document.getElementById('gpsTestModeInfo'); + if (infoDiv) infoDiv.style.display = 'none'; + const toggle = document.getElementById('gpsTestModeToggle'); + if (toggle) toggle.checked = false; + } + btn.textContent = 'Locating...'; updateStatus('Requesting GPS location...', 'info'); console.log('Starting GPS tracking...'); @@ -4777,6 +4811,11 @@ // Store user location for geocache proximity checks userLocation = { lat, lng, accuracy }; + // Sync test position on first fix so manual mode starts from real location + if (gpsFirstFix) { + testPosition = { lat, lng }; + } + // Check if dead player has reached home base for respawn checkHomeBaseRespawn(); @@ -4940,9 +4979,13 @@ const infoDiv = document.getElementById('gpsTestModeInfo'); if (enabled) { - // Initialize test position to current map center - const center = map.getCenter(); - testPosition = { lat: center.lat, lng: center.lng }; + // Initialize test position to user's current location if available, else map center + if (userLocation) { + testPosition = { lat: userLocation.lat, lng: userLocation.lng }; + } else { + const center = map.getCenter(); + testPosition = { lat: center.lat, lng: center.lng }; + } infoDiv.style.display = 'block'; updateTestPositionDisplay(); @@ -6554,6 +6597,8 @@ startHpRegenTimer(); } showNotification('Game settings updated - refreshing...', 'info'); + // Stop saving stats to prevent version conflicts during reload + statsLoadedFromServer = false; setTimeout(() => location.reload(), 1500); break; @@ -6561,6 +6606,8 @@ // Admin made a change - refresh the page console.log('Admin update:', data.changeType, data.details); showNotification(`Game data updated: ${data.changeType} - refreshing...`, 'info'); + // Stop saving stats to prevent version conflicts during reload + statsLoadedFromServer = false; setTimeout(() => location.reload(), 1500); break; @@ -11294,13 +11341,30 @@ } } else if (response.status === 409) { // Data conflict - our data is stale + // Instead of reloading, just fetch fresh stats from server and sync version const error = await response.json(); - console.error('Data conflict detected! Our version is stale.', error); - showNotification('⚠️ Data out of sync - refreshing...', 'error'); - // Reload from server to get fresh data - setTimeout(() => { - window.location.reload(); - }, 2000); + console.warn('Data conflict - syncing version from server...', error); + + // Fetch fresh stats from server (includes correct version) + try { + const freshResponse = await fetch('/api/user/rpg-stats', { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (freshResponse.ok) { + const freshStats = await freshResponse.json(); + if (freshStats && freshStats.dataVersion) { + // Update our version to match server + const oldVersion = playerStats.dataVersion; + playerStats.dataVersion = freshStats.dataVersion; + console.log(`Version synced: ${oldVersion} -> ${freshStats.dataVersion}`); + // Note: We keep local HP/MP/XP changes, just fix the version + // Next save will succeed with correct version + } + } + } catch (syncErr) { + console.error('Failed to sync version from server:', syncErr); + showNotification('Sync error - try refreshing', 'error'); + } } else { console.error('Server rejected stats save:', response.status); response.json().then(err => console.error('Server error:', err)); @@ -12434,6 +12498,8 @@ spawnTime: Date.now(), hp: monsterType.baseHp + (monsterLevel - 1) * monsterType.levelScale.hp, maxHp: monsterType.baseHp + (monsterLevel - 1) * monsterType.levelScale.hp, + mp: (monsterType.baseMp || 20) + (monsterLevel - 1) * (monsterType.levelScale.mp || 5), + maxMp: (monsterType.baseMp || 20) + (monsterLevel - 1) * (monsterType.levelScale.mp || 5), atk: monsterType.baseAtk + (monsterLevel - 1) * monsterType.levelScale.atk, def: monsterType.baseDef + (monsterLevel - 1) * monsterType.levelScale.def, marker: null, @@ -12588,12 +12654,18 @@ // Gather ALL monsters from entourage for multi-monster combat const monstersInCombat = monsterEntourage.map(m => { const monsterType = MONSTER_TYPES[m.type]; + // Calculate MP if not set (for monsters spawned before MP was added) + const baseMp = monsterType?.baseMp || 20; + const mpScale = monsterType?.levelScale?.mp || 5; + const calculatedMp = baseMp + (m.level - 1) * mpScale; return { id: m.id, type: m.type, level: m.level, hp: m.hp, maxHp: m.maxHp, + mp: m.mp ?? calculatedMp, + maxMp: m.maxMp ?? calculatedMp, atk: m.atk, def: m.def, accuracy: monsterType?.accuracy || 85, @@ -12778,6 +12850,7 @@ } const hpPct = Math.max(0, (monster.hp / monster.maxHp) * 100); + const mpPct = Math.max(0, (monster.mp / monster.maxMp) * 100); // Generate status overlay HTML for monster const monsterOverlayHtml = getMonsterStatusOverlayHtml(monster); @@ -12796,6 +12869,10 @@
HP: ${Math.max(0, monster.hp)}/${monster.maxHp}
+
+
+
MP: ${Math.max(0, monster.mp)}/${monster.maxMp}
+
`; // Click to select target (only if alive and player's turn) @@ -13207,8 +13284,18 @@ } // Select a skill using weighted random (or basic attack if none) - const selectedSkill = selectMonsterSkill(monster.type, monster.level); - console.log('[DEBUG] Selected skill:', selectedSkill?.id, selectedSkill?.name, 'type:', selectedSkill?.type); + // Pass monster's current MP to filter out skills they can't afford + const selectedSkill = selectMonsterSkill(monster.type, monster.level, monster.mp || 0); + console.log('[DEBUG] Selected skill:', selectedSkill?.id, selectedSkill?.name, 'type:', selectedSkill?.type, 'mpCost:', selectedSkill?.mpCost); + + // Deduct MP cost from monster + const skillMpCost = selectedSkill?.mpCost || 0; + if (skillMpCost > 0 && monster.mp !== undefined) { + monster.mp = Math.max(0, monster.mp - skillMpCost); + console.log('[DEBUG] Monster MP after skill:', monster.mp, '/', monster.maxMp); + // Update UI immediately to show MP decrease + updateCombatUI(); + } // Calculate hit chance const skillAccuracy = selectedSkill.accuracy || 85;