@ -2519,6 +2519,17 @@
font-size: 10px;
font-size: 10px;
margin-top: 2px;
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 {
.turn-indicator {
text-align: center;
text-align: center;
padding: 8px;
padding: 8px;
@ -3903,6 +3914,7 @@
let gpsAccuracyCircle = null;
let gpsAccuracyCircle = null;
let gpsFirstFix = true;
let gpsFirstFix = true;
let currentHeading = null;
let currentHeading = null;
let userLocation = null; // Last known GPS position {lat, lng, accuracy}
// GPS test mode (admin only)
// GPS test mode (admin only)
let gpsTestMode = false;
let gpsTestMode = false;
@ -4102,13 +4114,19 @@
baseHp: t.baseHp,
baseHp: t.baseHp,
baseAtk: t.baseAtk,
baseAtk: t.baseAtk,
baseDef: t.baseDef,
baseDef: t.baseDef,
baseMp: t.baseMp || 20,
xpReward: t.xpReward,
xpReward: t.xpReward,
accuracy: t.accuracy || 85,
accuracy: t.accuracy || 85,
dodge: t.dodge || 5,
dodge: t.dodge || 5,
minLevel: t.minLevel || 1,
minLevel: t.minLevel || 1,
maxLevel: t.maxLevel || 99,
maxLevel: t.maxLevel || 99,
spawnWeight: t.spawnWeight || 100,
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;
MONSTER_DIALOGUES[t.id] = t.dialogues;
});
});
@ -4316,18 +4334,18 @@
}
}
// Select a monster skill using weighted random
// Select a monster skill using weighted random
function selectMonsterSkill(monsterTypeId, monsterLevel) {
function selectMonsterSkill(monsterTypeId, monsterLevel, monsterMp = 999 ) {
const skills = MONSTER_SKILLS[monsterTypeId] || [];
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) {
if (validSkills.length === 0) {
// Fallback to basic attack
// Fallback to basic attack
console.log('[DEBUG] Using fallback basic_attack, SKILLS_DB has it:', !!SKILLS_DB['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
// Weighted random selection
@ -4344,7 +4362,8 @@
accuracy: skill.accuracy,
accuracy: skill.accuracy,
hitCount: skill.hitCount || 1,
hitCount: skill.hitCount || 1,
statusEffect: skill.statusEffect,
statusEffect: skill.statusEffect,
type: skill.type
type: skill.type,
mpCost: skill.mpCost || 0
};
};
}
}
}
}
@ -4358,7 +4377,8 @@
accuracy: lastSkill.accuracy,
accuracy: lastSkill.accuracy,
hitCount: lastSkill.hitCount || 1,
hitCount: lastSkill.hitCount || 1,
statusEffect: lastSkill.statusEffect,
statusEffect: lastSkill.statusEffect,
type: lastSkill.type
type: lastSkill.type,
mpCost: lastSkill.mpCost || 0
};
};
}
}
@ -4712,6 +4732,11 @@
gpsAccuracyCircle = null;
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;
gpsFirstFix = true;
btn.textContent = 'Show My Location';
btn.textContent = 'Show My Location';
btn.classList.remove('active');
btn.classList.remove('active');
@ -4723,6 +4748,15 @@
return;
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...';
btn.textContent = 'Locating...';
updateStatus('Requesting GPS location...', 'info');
updateStatus('Requesting GPS location...', 'info');
console.log('Starting GPS tracking...');
console.log('Starting GPS tracking...');
@ -4777,6 +4811,11 @@
// Store user location for geocache proximity checks
// Store user location for geocache proximity checks
userLocation = { lat, lng, accuracy };
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
// Check if dead player has reached home base for respawn
checkHomeBaseRespawn();
checkHomeBaseRespawn();
@ -4940,9 +4979,13 @@
const infoDiv = document.getElementById('gpsTestModeInfo');
const infoDiv = document.getElementById('gpsTestModeInfo');
if (enabled) {
if (enabled) {
// Initialize test position to current map center
// 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();
const center = map.getCenter();
testPosition = { lat: center.lat, lng: center.lng };
testPosition = { lat: center.lat, lng: center.lng };
}
infoDiv.style.display = 'block';
infoDiv.style.display = 'block';
updateTestPositionDisplay();
updateTestPositionDisplay();
@ -6554,6 +6597,8 @@
startHpRegenTimer();
startHpRegenTimer();
}
}
showNotification('Game settings updated - refreshing...', 'info');
showNotification('Game settings updated - refreshing...', 'info');
// Stop saving stats to prevent version conflicts during reload
statsLoadedFromServer = false;
setTimeout(() => location.reload(), 1500);
setTimeout(() => location.reload(), 1500);
break;
break;
@ -6561,6 +6606,8 @@
// Admin made a change - refresh the page
// Admin made a change - refresh the page
console.log('Admin update:', data.changeType, data.details);
console.log('Admin update:', data.changeType, data.details);
showNotification(`Game data updated: ${data.changeType} - refreshing...`, 'info');
showNotification(`Game data updated: ${data.changeType} - refreshing...`, 'info');
// Stop saving stats to prevent version conflicts during reload
statsLoadedFromServer = false;
setTimeout(() => location.reload(), 1500);
setTimeout(() => location.reload(), 1500);
break;
break;
@ -11294,13 +11341,30 @@
}
}
} else if (response.status === 409) {
} else if (response.status === 409) {
// Data conflict - our data is stale
// Data conflict - our data is stale
// Instead of reloading, just fetch fresh stats from server and sync version
const error = await response.json();
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 {
} else {
console.error('Server rejected stats save:', response.status);
console.error('Server rejected stats save:', response.status);
response.json().then(err => console.error('Server error:', err));
response.json().then(err => console.error('Server error:', err));
@ -12434,6 +12498,8 @@
spawnTime: Date.now(),
spawnTime: Date.now(),
hp: monsterType.baseHp + (monsterLevel - 1) * monsterType.levelScale.hp,
hp: monsterType.baseHp + (monsterLevel - 1) * monsterType.levelScale.hp,
maxHp: 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,
atk: monsterType.baseAtk + (monsterLevel - 1) * monsterType.levelScale.atk,
def: monsterType.baseDef + (monsterLevel - 1) * monsterType.levelScale.def,
def: monsterType.baseDef + (monsterLevel - 1) * monsterType.levelScale.def,
marker: null,
marker: null,
@ -12588,12 +12654,18 @@
// Gather ALL monsters from entourage for multi-monster combat
// Gather ALL monsters from entourage for multi-monster combat
const monstersInCombat = monsterEntourage.map(m => {
const monstersInCombat = monsterEntourage.map(m => {
const monsterType = MONSTER_TYPES[m.type];
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 {
return {
id: m.id,
id: m.id,
type: m.type,
type: m.type,
level: m.level,
level: m.level,
hp: m.hp,
hp: m.hp,
maxHp: m.maxHp,
maxHp: m.maxHp,
mp: m.mp ?? calculatedMp,
maxMp: m.maxMp ?? calculatedMp,
atk: m.atk,
atk: m.atk,
def: m.def,
def: m.def,
accuracy: monsterType?.accuracy || 85,
accuracy: monsterType?.accuracy || 85,
@ -12778,6 +12850,7 @@
}
}
const hpPct = Math.max(0, (monster.hp / monster.maxHp) * 100);
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
// Generate status overlay HTML for monster
const monsterOverlayHtml = getMonsterStatusOverlayHtml(monster);
const monsterOverlayHtml = getMonsterStatusOverlayHtml(monster);
@ -12796,6 +12869,10 @@
< div class = "hp-bar" > < div class = "hp-fill" style = "width: ${hpPct}%;" > < / div > < / div >
< div class = "hp-bar" > < div class = "hp-fill" style = "width: ${hpPct}%;" > < / div > < / div >
< div class = "stat-text" > HP: ${Math.max(0, monster.hp)}/${monster.maxHp}< / div >
< div class = "stat-text" > HP: ${Math.max(0, monster.hp)}/${monster.maxHp}< / div >
< / div >
< / div >
< div class = "monster-entry-mp" >
< div class = "mp-bar" > < div class = "mp-fill" style = "width: ${mpPct}%;" > < / div > < / div >
< div class = "stat-text" > MP: ${Math.max(0, monster.mp)}/${monster.maxMp}< / div >
< / div >
`;
`;
// Click to select target (only if alive and player's turn)
// 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)
// 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
// Calculate hit chance
const skillAccuracy = selectedSkill.accuracy || 85;
const skillAccuracy = selectedSkill.accuracy || 85;