@ -324,6 +324,42 @@ class HikeMapDB {
)
` );
// OSM Tags table - configuration for location-based prefixes
this . db . exec ( `
CREATE TABLE IF NOT EXISTS osm_tags (
id TEXT PRIMARY KEY ,
prefixes TEXT NOT NULL DEFAULT '[]' ,
icon TEXT DEFAULT 'map-marker' ,
visibility_distance INTEGER DEFAULT 400 ,
spawn_radius INTEGER DEFAULT 400 ,
enabled BOOLEAN DEFAULT 0 ,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
` );
// OSM Tag settings - global prefix configuration
this . db . exec ( `
CREATE TABLE IF NOT EXISTS osm_tag_settings (
key TEXT PRIMARY KEY ,
value TEXT NOT NULL ,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
` );
// Player monster kills - tracking for quests/bestiary
this . db . exec ( `
CREATE TABLE IF NOT EXISTS player_monster_kills (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
player_id INTEGER NOT NULL ,
monster_name TEXT NOT NULL ,
kill_count INTEGER DEFAULT 1 ,
first_kill DATETIME DEFAULT CURRENT_TIMESTAMP ,
last_kill DATETIME DEFAULT CURRENT_TIMESTAMP ,
FOREIGN KEY ( player_id ) REFERENCES users ( id ) ,
UNIQUE ( player_id , monster_name )
)
` );
// Create indexes for performance
this . db . exec ( `
CREATE INDEX IF NOT EXISTS idx_geocache_finds_user ON geocache_finds ( user_id ) ;
@ -339,6 +375,7 @@ class HikeMapDB {
CREATE INDEX IF NOT EXISTS idx_class_skills_class ON class_skills ( class_id ) ;
CREATE INDEX IF NOT EXISTS idx_class_skills_skill ON class_skills ( skill_id ) ;
CREATE INDEX IF NOT EXISTS idx_player_buffs_player ON player_buffs ( player_id ) ;
CREATE INDEX IF NOT EXISTS idx_player_monster_kills_player ON player_monster_kills ( player_id ) ;
` );
}
@ -2210,6 +2247,206 @@ class HikeMapDB {
return stmt . run ( threshold , threshold ) ;
}
// =====================
// OSM TAGS METHODS
// =====================
getAllOsmTags ( enabledOnly = false ) {
const stmt = enabledOnly
? this . db . prepare ( ` SELECT * FROM osm_tags WHERE enabled = 1 ` )
: this . db . prepare ( ` SELECT * FROM osm_tags ` ) ;
return stmt . all ( ) ;
}
getOsmTag ( id ) {
const stmt = this . db . prepare ( ` SELECT * FROM osm_tags WHERE id = ? ` ) ;
return stmt . get ( id ) ;
}
createOsmTag ( tagData ) {
const stmt = this . db . prepare ( `
INSERT INTO osm_tags ( id , prefixes , icon , visibility_distance , spawn_radius , enabled )
VALUES ( ? , ? , ? , ? , ? , ? )
` );
const prefixes = Array . isArray ( tagData . prefixes )
? JSON . stringify ( tagData . prefixes )
: ( tagData . prefixes || '[]' ) ;
// Auto-enable if prefixes exist
const parsedPrefixes = typeof prefixes === 'string' ? JSON . parse ( prefixes ) : prefixes ;
const enabled = parsedPrefixes . length > 0 ? 1 : 0 ;
return stmt . run (
tagData . id ,
prefixes ,
tagData . icon || 'map-marker' ,
tagData . visibility_distance || tagData . visibilityDistance || 400 ,
tagData . spawn_radius || tagData . spawnRadius || 400 ,
enabled
) ;
}
updateOsmTag ( id , tagData ) {
const stmt = this . db . prepare ( `
UPDATE osm_tags SET
prefixes = ? , icon = ? , visibility_distance = ? , spawn_radius = ? , enabled = ?
WHERE id = ?
` );
const prefixes = Array . isArray ( tagData . prefixes )
? JSON . stringify ( tagData . prefixes )
: ( tagData . prefixes || '[]' ) ;
// Auto-enable if prefixes exist
const parsedPrefixes = typeof prefixes === 'string' ? JSON . parse ( prefixes ) : prefixes ;
const enabled = parsedPrefixes . length > 0 ? 1 : 0 ;
return stmt . run (
prefixes ,
tagData . icon || 'map-marker' ,
tagData . visibility_distance || tagData . visibilityDistance || 400 ,
tagData . spawn_radius || tagData . spawnRadius || 400 ,
enabled ,
id
) ;
}
deleteOsmTag ( id ) {
const stmt = this . db . prepare ( ` DELETE FROM osm_tags WHERE id = ? ` ) ;
return stmt . run ( id ) ;
}
// =====================
// OSM TAG SETTINGS METHODS
// =====================
getOsmTagSetting ( key ) {
const stmt = this . db . prepare ( ` SELECT value FROM osm_tag_settings WHERE key = ? ` ) ;
const row = stmt . get ( key ) ;
if ( ! row ) return null ;
try {
return JSON . parse ( row . value ) ;
} catch {
return row . value ;
}
}
setOsmTagSetting ( key , value ) {
const stmt = this . db . prepare ( `
INSERT INTO osm_tag_settings ( key , value , updated_at )
VALUES ( ? , ? , datetime ( 'now' ) )
ON CONFLICT ( key ) DO UPDATE SET value = excluded . value , updated_at = datetime ( 'now' )
` );
const valueStr = typeof value === 'string' ? value : JSON . stringify ( value ) ;
return stmt . run ( key , valueStr ) ;
}
getAllOsmTagSettings ( ) {
const stmt = this . db . prepare ( ` SELECT key, value FROM osm_tag_settings ` ) ;
const rows = stmt . all ( ) ;
const settings = { } ;
rows . forEach ( row => {
try {
settings [ row . key ] = JSON . parse ( row . value ) ;
} catch {
settings [ row . key ] = row . value ;
}
} ) ;
return settings ;
}
seedDefaultOsmTagSettings ( ) {
const defaults = {
basePrefixChance : 25 ,
doublePrefixChance : 10
} ;
for ( const [ key , value ] of Object . entries ( defaults ) ) {
const existing = this . getOsmTagSetting ( key ) ;
if ( existing === null ) {
this . setOsmTagSetting ( key , value ) ;
console . log ( ` Seeded OSM tag setting: ${ key } = ${ value } ` ) ;
}
}
}
seedDefaultOsmTags ( ) {
console . log ( 'Checking/seeding default OSM tags...' ) ;
const defaultTags = [
{ id : 'grocery' , icon : 'cart' , prefixes : [ ] } ,
{ id : 'restaurant' , icon : 'silverware-fork-knife' , prefixes : [ ] } ,
{ id : 'fastfood' , icon : 'food' , prefixes : [ ] } ,
{ id : 'cafe' , icon : 'coffee' , prefixes : [ ] } ,
{ id : 'bar' , icon : 'glass-mug-variant' , prefixes : [ ] } ,
{ id : 'pharmacy' , icon : 'pharmacy' , prefixes : [ ] } ,
{ id : 'bank' , icon : 'bank' , prefixes : [ ] } ,
{ id : 'convenience' , icon : 'store' , prefixes : [ ] } ,
{ id : 'park' , icon : 'tree' , prefixes : [ ] } ,
{ id : 'gasstation' , icon : 'gas-station' , prefixes : [ ] }
] ;
for ( const tag of defaultTags ) {
const existing = this . getOsmTag ( tag . id ) ;
if ( ! existing ) {
this . createOsmTag ( tag ) ;
console . log ( ` Seeded OSM tag: ${ tag . id } ` ) ;
}
}
// Also seed default settings
this . seedDefaultOsmTagSettings ( ) ;
}
// =====================
// PLAYER MONSTER KILLS METHODS
// =====================
recordMonsterKill ( playerId , monsterName ) {
const stmt = this . db . prepare ( `
INSERT INTO player_monster_kills ( player_id , monster_name , kill_count , first_kill , last_kill )
VALUES ( ? , ? , 1 , datetime ( 'now' ) , datetime ( 'now' ) )
ON CONFLICT ( player_id , monster_name ) DO UPDATE SET
kill_count = kill_count + 1 ,
last_kill = datetime ( 'now' )
` );
return stmt . run ( playerId , monsterName ) ;
}
getPlayerKillStats ( playerId ) {
const stmt = this . db . prepare ( `
SELECT monster_name , kill_count , first_kill , last_kill
FROM player_monster_kills
WHERE player_id = ?
ORDER BY kill_count DESC
` );
return stmt . all ( playerId ) ;
}
getPlayerKillsForMonster ( playerId , monsterName ) {
const stmt = this . db . prepare ( `
SELECT * FROM player_monster_kills
WHERE player_id = ? AND monster_name = ?
` );
return stmt . get ( playerId , monsterName ) ;
}
getTotalPlayerKills ( playerId ) {
const stmt = this . db . prepare ( `
SELECT SUM ( kill_count ) as total FROM player_monster_kills WHERE player_id = ?
` );
const result = stmt . get ( playerId ) ;
return result ? result . total || 0 : 0 ;
}
getTopKillers ( limit = 50 ) {
const stmt = this . db . prepare ( `
SELECT u . id , u . username , u . avatar_icon , u . avatar_color ,
SUM ( k . kill_count ) as total_kills ,
COUNT ( DISTINCT k . monster_name ) as unique_monsters
FROM users u
JOIN player_monster_kills k ON u . id = k . player_id
GROUP BY u . id
ORDER BY total_kills DESC
LIMIT ?
` );
return stmt . all ( limit ) ;
}
close ( ) {
if ( this . db ) {
this . db . close ( ) ;