@ -2649,6 +2649,12 @@
font-weight: bold;
font-weight: bold;
flex: 1;
flex: 1;
}
}
.monster-entry-name.prefix-1 {
font-size: 11px;
}
.monster-entry-name.prefix-2 {
font-size: 10px;
}
.monster-entry-hp {
.monster-entry-hp {
margin-top: 4px;
margin-top: 4px;
}
}
@ -4424,6 +4430,104 @@
let MONSTER_SKILLS = {}; // Skills assigned to each monster type
let MONSTER_SKILLS = {}; // Skills assigned to each monster type
let skillsLoaded = false;
let skillsLoaded = false;
// OSM Tags for location-based prefixes
let OSM_TAGS = {};
let OSM_TAG_SETTINGS = { basePrefixChance: 25, doublePrefixChance: 10 };
let osmTagsLoaded = false;
// Load OSM tags from the database
async function loadOsmTags() {
try {
const response = await fetch('/api/osm-tags');
if (response.ok) {
const tags = await response.json();
OSM_TAGS = {};
tags.forEach(t => {
const prefixes = typeof t.prefixes === 'string' ? JSON.parse(t.prefixes || '[]') : (t.prefixes || []);
OSM_TAGS[t.id] = {
prefixes: prefixes,
icon: t.icon,
visibilityDistance: t.visibility_distance,
spawnRadius: t.spawn_radius,
enabled: t.enabled
};
});
osmTagsLoaded = true;
console.log(`Loaded ${Object.keys(OSM_TAGS).length} OSM tags`);
}
// Also load settings
const settingsResponse = await fetch('/api/osm-tag-settings');
if (settingsResponse.ok) {
const settings = await settingsResponse.json();
if (settings.basePrefixChance !== undefined) {
OSM_TAG_SETTINGS.basePrefixChance = settings.basePrefixChance;
}
if (settings.doublePrefixChance !== undefined) {
OSM_TAG_SETTINGS.doublePrefixChance = settings.doublePrefixChance;
}
console.log('Loaded OSM tag settings:', OSM_TAG_SETTINGS);
}
} catch (err) {
console.error('Failed to load OSM tags:', err);
}
}
// Record a monster kill for tracking (quests/bestiary)
async function recordMonsterKill(monster) {
const fullName = (monster.namePrefix || '') + monster.data.name;
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
await fetch('/api/user/monster-kill', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ monsterName: fullName })
});
} catch (err) {
console.error('Failed to record kill:', err);
}
}
// Discover nearby locations when homebase is set
async function discoverNearbyLocations(lat, lng) {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
updateStatus('Discovering nearby locations...', 'info');
const response = await fetch('/api/geocaches/discover-nearby', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ lat, lng, radiusMiles: 2 })
});
if (response.ok) {
const { discovered, added } = await response.json();
if (added > 0) {
updateStatus(`Discovered ${added} new locations near your home base!`, 'success');
// Reload geocaches to include new ones
await loadGeocaches();
renderGeocaches();
} else if (discovered > 0) {
updateStatus(`Found ${discovered} locations already in database.`, 'info');
} else {
updateStatus('Home base set!', 'success');
}
}
} catch (err) {
console.error('Failed to discover locations:', err);
}
}
// Load monster types from the database
// Load monster types from the database
async function loadMonsterTypes() {
async function loadMonsterTypes() {
try {
try {
@ -12890,6 +12994,9 @@
playerStats.homeBaseLng = data.homeBaseLng;
playerStats.homeBaseLng = data.homeBaseLng;
updateHomeBaseMarker();
updateHomeBaseMarker();
console.log('Home base set at:', lat, lng);
console.log('Home base set at:', lat, lng);
// Discover nearby locations via Overpass API
discoverNearbyLocations(lat, lng);
} else {
} else {
const error = await response.json();
const error = await response.json();
alert(error.error || 'Failed to set home base');
alert(error.error || 'Failed to set home base');
@ -13603,23 +13710,58 @@
def: monsterType.baseDef + (monsterLevel - 1) * monsterType.levelScale.def,
def: monsterType.baseDef + (monsterLevel - 1) * monsterType.levelScale.def,
marker: null,
marker: null,
lastDialogueTime: 0,
lastDialogueTime: 0,
namePrefix: '' // Will be set below based on location
namePrefix: '', // Will be set below based on location
prefixes: [] // Array of prefix strings for font sizing
};
};
// Check if spawning near a special location and set name prefix
// Check if spawning near a special location and set name prefix
// Only apply prefix to "anywhere" monsters - location-specific monsters don't need it
// Only apply prefix to "anywhere" monsters - location-specific monsters don't need it
const monsterSpawnLoc = monsterType.spawnLocation || 'anywhere';
const monsterSpawnLoc = monsterType.spawnLocation || 'anywhere';
if (monsterSpawnLoc === 'anywhere') {
if (monsterSpawnLoc === 'anywhere' & & osmTagsLoaded ) {
const spawnPos = L.latLng(monster.position.lat, monster.position.lng);
const spawnPos = L.latLng(monster.position.lat, monster.position.lng);
// Collect all eligible prefixes from nearby tagged locations
const eligiblePrefixes = [];
for (const cache of geocaches) {
for (const cache of geocaches) {
// Use tags if available, fall back to icon check for backwards compatibility
const isGrocery = (cache.tags & & cache.tags.includes('grocery')) || cache.icon === 'cart';
if (isGrocery) {
if (!cache.tags || cache.tags.length === 0) continue;
for (const tagId of cache.tags) {
const tagConfig = OSM_TAGS[tagId];
if (!tagConfig || !tagConfig.enabled) continue;
if (!tagConfig.prefixes || tagConfig.prefixes.length === 0) continue;
const dist = spawnPos.distanceTo(L.latLng(cache.lat, cache.lng));
const dist = spawnPos.distanceTo(L.latLng(cache.lat, cache.lng));
if (dist < = 400) {
monster.namePrefix = "Cart Wranglin' ";
break;
if (dist < = tagConfig.spawnRadius) {
// Add all prefixes from this tag (avoiding duplicates)
tagConfig.prefixes.forEach(prefix => {
if (!eligiblePrefixes.find(p => p.prefix === prefix)) {
eligiblePrefixes.push({ prefix, tagId });
}
});
}
}
}
if (eligiblePrefixes.length > 0) {
// Roll for prefix chance
if (Math.random() * 100 < OSM_TAG_SETTINGS.basePrefixChance ) {
// Pick first prefix
const firstPick = eligiblePrefixes[Math.floor(Math.random() * eligiblePrefixes.length)];
monster.prefixes.push(firstPick.prefix);
// Check for double prefix (need at least 2 different prefixes available)
if (eligiblePrefixes.length >= 2 & & Math.random() * 100 < OSM_TAG_SETTINGS.doublePrefixChance ) {
const remainingPrefixes = eligiblePrefixes.filter(p => p.prefix !== firstPick.prefix);
if (remainingPrefixes.length > 0) {
const secondPick = remainingPrefixes[Math.floor(Math.random() * remainingPrefixes.length)];
monster.prefixes.push(secondPick.prefix);
}
}
}
// Randomize order and build display string
monster.prefixes.sort(() => Math.random() - 0.5);
monster.namePrefix = monster.prefixes.map(p => p + ' ').join('');
}
}
}
}
}
}
@ -13794,7 +13936,8 @@
accuracy: monsterType?.accuracy || 85,
accuracy: monsterType?.accuracy || 85,
dodge: monsterType?.dodge || 5,
dodge: monsterType?.dodge || 5,
data: monsterType,
data: monsterType,
namePrefix: m.namePrefix || '' // Location-based name prefix
namePrefix: m.namePrefix || '', // Location-based name prefix
prefixes: m.prefixes || [] // Array of prefix strings for font sizing
};
};
});
});
@ -14090,7 +14233,7 @@
onerror="this.src='/mapgameimgs/monsters/default100.png'" alt="${monster.namePrefix || ''}${monster.data.name}">
onerror="this.src='/mapgameimgs/monsters/default100.png'" alt="${monster.namePrefix || ''}${monster.data.name}">
< div class = "status-overlay" > ${monsterOverlayHtml}< / div >
< div class = "status-overlay" > ${monsterOverlayHtml}< / div >
< / div >
< / div >
< span class = "monster-entry-name" > ${monster.namePrefix || ''}${monster.data.name} Lv.${monster.level}< / span >
< span class = "monster-entry-name${monster.prefixes?.length >= 2 ? ' prefix-2' : (monster.prefixes?.length === 1 || monster.namePrefix ? ' prefix-1' : '')} " > ${monster.namePrefix || ''}${monster.data.name} Lv.${monster.level}< / span >
< / div >
< / div >
< div class = "monster-entry-hp" >
< div class = "monster-entry-hp" >
< 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 >
@ -14228,6 +14371,7 @@
// Check if this killed the monster
// Check if this killed the monster
if (currentTarget.hp < = 0) {
if (currentTarget.hp < = 0) {
monstersKilled++;
monstersKilled++;
recordMonsterKill(currentTarget); // Track kill for bestiary
// Play death animation
// Play death animation
animateMonsterAttack(targetIndex, 'death');
animateMonsterAttack(targetIndex, 'death');
playSfx('monster_death');
playSfx('monster_death');
@ -14589,6 +14733,7 @@
// Check if this monster died
// Check if this monster died
if (currentTarget.hp < = 0) {
if (currentTarget.hp < = 0) {
monstersKilled++;
monstersKilled++;
recordMonsterKill(currentTarget); // Track kill for bestiary
playSfx('monster_death');
playSfx('monster_death');
// Award XP immediately for this kill
// Award XP immediately for this kill
const xpReward = (currentTarget.data?.xpReward || 10) * currentTarget.level;
const xpReward = (currentTarget.data?.xpReward || 10) * currentTarget.level;
@ -15010,7 +15155,7 @@
// ==========================================
// ==========================================
// Load monster types, skills, classes, and spawn settings from database, then initialize auth
// Load monster types, skills, classes, and spawn settings from database, then initialize auth
Promise.all([loadMonsterTypes(), loadSkillsFromDatabase(), loadClasses(), loadSpawnSettings()]).then(() => {
Promise.all([loadMonsterTypes(), loadSkillsFromDatabase(), loadClasses(), loadSpawnSettings(), loadOsmTags() ]).then(() => {
loadCurrentUser();
loadCurrentUser();
});
});