@ -124,12 +124,35 @@ class HikeMapDB {
level_scale_hp INTEGER NOT NULL ,
level_scale_hp INTEGER NOT NULL ,
level_scale_atk INTEGER NOT NULL ,
level_scale_atk INTEGER NOT NULL ,
level_scale_def INTEGER NOT NULL ,
level_scale_def INTEGER NOT NULL ,
min_level INTEGER DEFAULT 1 ,
max_level INTEGER DEFAULT 5 ,
spawn_weight INTEGER DEFAULT 100 ,
dialogues TEXT NOT NULL ,
dialogues TEXT NOT NULL ,
enabled BOOLEAN DEFAULT 1 ,
enabled BOOLEAN DEFAULT 1 ,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
)
` );
` );
// Add columns if they don't exist (migration for existing databases)
try {
this . db . exec ( ` ALTER TABLE monster_types ADD COLUMN min_level INTEGER DEFAULT 1 ` ) ;
} catch ( e ) { /* Column already exists */ }
try {
this . db . exec ( ` ALTER TABLE monster_types ADD COLUMN max_level INTEGER DEFAULT 5 ` ) ;
} catch ( e ) { /* Column already exists */ }
try {
this . db . exec ( ` ALTER TABLE monster_types ADD COLUMN spawn_weight INTEGER DEFAULT 100 ` ) ;
} catch ( e ) { /* Column already exists */ }
// Game settings table - key/value store for game configuration
this . db . exec ( `
CREATE TABLE IF NOT EXISTS game_settings (
key TEXT PRIMARY KEY ,
value TEXT NOT NULL ,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
` );
// Create indexes for performance
// Create indexes for performance
this . db . exec ( `
this . db . exec ( `
CREATE INDEX IF NOT EXISTS idx_geocache_finds_user ON geocache_finds ( user_id ) ;
CREATE INDEX IF NOT EXISTS idx_geocache_finds_user ON geocache_finds ( user_id ) ;
@ -526,21 +549,38 @@ class HikeMapDB {
createMonsterType ( monsterData ) {
createMonsterType ( monsterData ) {
const stmt = this . db . prepare ( `
const stmt = this . db . prepare ( `
INSERT INTO monster_types ( id , name , icon , base_hp , base_atk , base_def , xp_reward , level_scale_hp , level_scale_atk , level_scale_def , dialogues , enabled )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
` );
INSERT INTO monster_types ( id , name , icon , base_hp , base_atk , base_def , xp_reward ,
level_scale_hp , level_scale_atk , level_scale_def , min_level , max_level , spawn_weight , dialogues , enabled )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
` );
// Support both camelCase (legacy) and snake_case (new admin UI) field names
const baseHp = monsterData . baseHp || monsterData . base_hp ;
const baseAtk = monsterData . baseAtk || monsterData . base_atk ;
const baseDef = monsterData . baseDef || monsterData . base_def ;
const xpReward = monsterData . xpReward || monsterData . base_xp ;
const levelScale = monsterData . levelScale || { hp : 10 , atk : 2 , def : 1 } ;
const minLevel = monsterData . minLevel || monsterData . min_level || 1 ;
const maxLevel = monsterData . maxLevel || monsterData . max_level || 5 ;
const spawnWeight = monsterData . spawnWeight || monsterData . spawn_weight || 100 ;
const dialogues = typeof monsterData . dialogues === 'string'
? monsterData . dialogues
: JSON . stringify ( monsterData . dialogues ) ;
return stmt . run (
return stmt . run (
monsterData . id ,
monsterData . id || monsterData . key ,
monsterData . name ,
monsterData . name ,
monsterData . icon ,
monsterData . baseHp ,
monsterData . baseAtk ,
monsterData . baseDef ,
monsterData . xpReward ,
monsterData . levelScale . hp ,
monsterData . levelScale . atk ,
monsterData . levelScale . def ,
JSON . stringify ( monsterData . dialogues ) ,
monsterData . icon || '🟢' ,
baseHp ,
baseAtk ,
baseDef ,
xpReward ,
levelScale . hp ,
levelScale . atk ,
levelScale . def ,
minLevel ,
maxLevel ,
spawnWeight ,
dialogues ,
monsterData . enabled !== false ? 1 : 0
monsterData . enabled !== false ? 1 : 0
) ;
) ;
}
}
@ -550,20 +590,36 @@ class HikeMapDB {
UPDATE monster_types SET
UPDATE monster_types SET
name = ? , icon = ? , base_hp = ? , base_atk = ? , base_def = ? ,
name = ? , icon = ? , base_hp = ? , base_atk = ? , base_def = ? ,
xp_reward = ? , level_scale_hp = ? , level_scale_atk = ? , level_scale_def = ? ,
xp_reward = ? , level_scale_hp = ? , level_scale_atk = ? , level_scale_def = ? ,
dialogues = ? , enabled = ?
min_level = ? , max_level = ? , spawn_weight = ? , dialogues = ? , enabled = ?
WHERE id = ?
WHERE id = ?
` );
` );
// Support both camelCase (legacy) and snake_case (new admin UI) field names
const baseHp = monsterData . baseHp || monsterData . base_hp ;
const baseAtk = monsterData . baseAtk || monsterData . base_atk ;
const baseDef = monsterData . baseDef || monsterData . base_def ;
const xpReward = monsterData . xpReward || monsterData . base_xp ;
const levelScale = monsterData . levelScale || { hp : 10 , atk : 2 , def : 1 } ;
const minLevel = monsterData . minLevel || monsterData . min_level || 1 ;
const maxLevel = monsterData . maxLevel || monsterData . max_level || 5 ;
const spawnWeight = monsterData . spawnWeight || monsterData . spawn_weight || 100 ;
const dialogues = typeof monsterData . dialogues === 'string'
? monsterData . dialogues
: JSON . stringify ( monsterData . dialogues ) ;
return stmt . run (
return stmt . run (
monsterData . name ,
monsterData . name ,
monsterData . icon ,
monsterData . baseHp ,
monsterData . baseAtk ,
monsterData . baseDef ,
monsterData . xpReward ,
monsterData . levelScale . hp ,
monsterData . levelScale . atk ,
monsterData . levelScale . def ,
JSON . stringify ( monsterData . dialogues ) ,
monsterData . icon || '🟢' ,
baseHp ,
baseAtk ,
baseDef ,
xpReward ,
levelScale . hp ,
levelScale . atk ,
levelScale . def ,
minLevel ,
maxLevel ,
spawnWeight ,
dialogues ,
monsterData . enabled !== false ? 1 : 0 ,
monsterData . enabled !== false ? 1 : 0 ,
id
id
) ;
) ;
@ -631,6 +687,107 @@ class HikeMapDB {
console . log ( 'Seeded default monster: Moop' ) ;
console . log ( 'Seeded default monster: Moop' ) ;
}
}
// Admin: Get all users with their RPG stats
getAllUsers ( ) {
const stmt = this . db . prepare ( `
SELECT u . id , u . username , u . email , u . created_at , u . total_points , u . finds_count ,
u . avatar_icon , u . avatar_color , u . is_admin ,
r . character_name , r . race , r . class , r . level , r . xp , r . hp , r . max_hp ,
r . mp , r . max_mp , r . atk , r . def , r . unlocked_skills
FROM users u
LEFT JOIN rpg_stats r ON u . id = r . user_id
ORDER BY u . created_at DESC
` );
return stmt . all ( ) ;
}
// Admin: Update user RPG stats
updateUserRpgStats ( userId , stats ) {
const stmt = this . db . prepare ( `
UPDATE rpg_stats SET
level = ? , xp = ? , hp = ? , max_hp = ? , mp = ? , max_mp = ? ,
atk = ? , def = ? , unlocked_skills = ? , updated_at = datetime ( 'now' )
WHERE user_id = ?
` );
const unlockedSkillsJson = stats . unlockedSkills ? JSON . stringify ( stats . unlockedSkills ) : '["basic_attack"]' ;
return stmt . run (
stats . level || 1 ,
stats . xp || 0 ,
stats . hp || 100 ,
stats . maxHp || 100 ,
stats . mp || 50 ,
stats . maxMp || 50 ,
stats . atk || 12 ,
stats . def || 8 ,
unlockedSkillsJson ,
userId
) ;
}
// Admin: Reset user RPG progress
resetUserProgress ( userId ) {
const stmt = this . db . prepare ( `
UPDATE rpg_stats SET
level = 1 , xp = 0 , hp = 100 , max_hp = 100 , mp = 50 , max_mp = 50 ,
atk = 12 , def = 8 , unlocked_skills = '["basic_attack"]' ,
updated_at = datetime ( 'now' )
WHERE user_id = ?
` );
const result = stmt . run ( userId ) ;
// Also clear their monster entourage
this . db . prepare ( ` DELETE FROM monster_entourage WHERE user_id = ? ` ) . run ( userId ) ;
return result ;
}
// Game settings methods
getSetting ( key ) {
const stmt = this . db . prepare ( ` SELECT value FROM game_settings WHERE key = ? ` ) ;
const row = stmt . get ( key ) ;
return row ? row . value : null ;
}
setSetting ( key , value ) {
const stmt = this . db . prepare ( `
INSERT INTO game_settings ( key , value , updated_at )
VALUES ( ? , ? , datetime ( 'now' ) )
ON CONFLICT ( key ) DO UPDATE SET value = excluded . value , updated_at = datetime ( 'now' )
` );
return stmt . run ( key , value ) ;
}
getAllSettings ( ) {
const stmt = this . db . prepare ( ` SELECT key, value FROM game_settings ` ) ;
const rows = stmt . all ( ) ;
const settings = { } ;
rows . forEach ( row => {
// Try to parse JSON values
try {
settings [ row . key ] = JSON . parse ( row . value ) ;
} catch {
settings [ row . key ] = row . value ;
}
} ) ;
return settings ;
}
seedDefaultSettings ( ) {
const defaults = {
monsterSpawnInterval : 30000 ,
maxMonstersPerPlayer : 10 ,
xpMultiplier : 1.0 ,
combatEnabled : true
} ;
for ( const [ key , value ] of Object . entries ( defaults ) ) {
const existing = this . getSetting ( key ) ;
if ( existing === null ) {
this . setSetting ( key , JSON . stringify ( value ) ) ;
}
}
}
close ( ) {
close ( ) {
if ( this . db ) {
if ( this . db ) {
this . db . close ( ) ;
this . db . close ( ) ;