diff --git a/database.js b/database.js index d429a44..20f62b2 100644 --- a/database.js +++ b/database.js @@ -88,6 +88,9 @@ class HikeMapDB { try { this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN race TEXT DEFAULT 'human'`); } catch (e) { /* Column already exists */ } + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN unlocked_skills TEXT DEFAULT '["basic_attack"]'`); + } catch (e) { /* Column already exists */ } // Monster entourage table - stores monsters following the player this.db.exec(` @@ -108,6 +111,25 @@ class HikeMapDB { ) `); + // Monster types table - defines available monster types + this.db.exec(` + CREATE TABLE IF NOT EXISTS monster_types ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + icon TEXT NOT NULL, + base_hp INTEGER NOT NULL, + base_atk INTEGER NOT NULL, + base_def INTEGER NOT NULL, + xp_reward INTEGER NOT NULL, + level_scale_hp INTEGER NOT NULL, + level_scale_atk INTEGER NOT NULL, + level_scale_def INTEGER NOT NULL, + dialogues TEXT NOT NULL, + enabled BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + // Create indexes for performance this.db.exec(` CREATE INDEX IF NOT EXISTS idx_geocache_finds_user ON geocache_finds(user_id); @@ -354,7 +376,7 @@ class HikeMapDB { // RPG Stats methods getRpgStats(userId) { const stmt = this.db.prepare(` - SELECT character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def + SELECT character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, unlocked_skills FROM rpg_stats WHERE user_id = ? `); return stmt.get(userId); @@ -369,8 +391,8 @@ class HikeMapDB { createCharacter(userId, characterData) { const stmt = this.db.prepare(` - INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, unlocked_skills, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(user_id) DO UPDATE SET character_name = excluded.character_name, race = excluded.race, @@ -383,8 +405,11 @@ class HikeMapDB { max_mp = excluded.max_mp, atk = excluded.atk, def = excluded.def, + unlocked_skills = excluded.unlocked_skills, updated_at = datetime('now') `); + // New characters start with only basic_attack + const unlockedSkillsJson = JSON.stringify(characterData.unlockedSkills || ['basic_attack']); return stmt.run( userId, characterData.name, @@ -397,14 +422,15 @@ class HikeMapDB { characterData.mp || 50, characterData.maxMp || 50, characterData.atk || 12, - characterData.def || 8 + characterData.def || 8, + unlockedSkillsJson ); } saveRpgStats(userId, stats) { const stmt = this.db.prepare(` - INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, unlocked_skills, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(user_id) DO UPDATE SET character_name = COALESCE(excluded.character_name, rpg_stats.character_name), race = COALESCE(excluded.race, rpg_stats.race), @@ -417,8 +443,11 @@ class HikeMapDB { max_mp = excluded.max_mp, atk = excluded.atk, def = excluded.def, + unlocked_skills = COALESCE(excluded.unlocked_skills, rpg_stats.unlocked_skills), updated_at = datetime('now') `); + // Convert unlockedSkills array to JSON string for storage + const unlockedSkillsJson = stats.unlockedSkills ? JSON.stringify(stats.unlockedSkills) : null; return stmt.run( userId, stats.name || null, @@ -431,7 +460,8 @@ class HikeMapDB { stats.mp || 50, stats.maxMp || 50, stats.atk || 12, - stats.def || 8 + stats.def || 8, + unlockedSkillsJson ); } @@ -481,6 +511,126 @@ class HikeMapDB { return stmt.run(userId, monsterId); } + // Monster type methods + getAllMonsterTypes(enabledOnly = true) { + const stmt = enabledOnly + ? this.db.prepare(`SELECT * FROM monster_types WHERE enabled = 1`) + : this.db.prepare(`SELECT * FROM monster_types`); + return stmt.all(); + } + + getMonsterType(id) { + const stmt = this.db.prepare(`SELECT * FROM monster_types WHERE id = ?`); + return stmt.get(id); + } + + createMonsterType(monsterData) { + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + return stmt.run( + monsterData.id, + 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.enabled !== false ? 1 : 0 + ); + } + + updateMonsterType(id, monsterData) { + const stmt = this.db.prepare(` + UPDATE monster_types SET + name = ?, icon = ?, base_hp = ?, base_atk = ?, base_def = ?, + xp_reward = ?, level_scale_hp = ?, level_scale_atk = ?, level_scale_def = ?, + dialogues = ?, enabled = ? + WHERE id = ? + `); + return stmt.run( + 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.enabled !== false ? 1 : 0, + id + ); + } + + deleteMonsterType(id) { + const stmt = this.db.prepare(`DELETE FROM monster_types WHERE id = ?`); + return stmt.run(id); + } + + seedDefaultMonsters() { + // Check if Moop already exists + const existing = this.getMonsterType('moop'); + if (existing) return; + + // Seed Moop - Matter Out Of Place + this.createMonsterType({ + id: 'moop', + name: 'Moop', + icon: '🟢', + baseHp: 30, + baseAtk: 5, + baseDef: 2, + xpReward: 15, + levelScale: { hp: 10, atk: 2, def: 1 }, + dialogues: { + annoyed: [ + "Hey! HEY! I don't belong here!", + "Excuse me, I'm MATTER OUT OF PLACE!", + "This is a Leave No Trace trail!", + "I was in your pocket! YOUR POCKET!", + "Pack it in, pack it out! Remember?!" + ], + frustrated: [ + "STOP IGNORING ME!", + "I don't belong here and YOU know it!", + "I'm literally the definition of litter!", + "MOOP! MATTER! OUT! OF! PLACE!", + "Fine! Just keep walking! SEE IF I CARE!" + ], + desperate: [ + "Please... just pick me up...", + "I promise I'll fit in your pocket!", + "What if I promised to be biodegradable?", + "I just want to go to a proper bin...", + "I didn't ask to be abandoned here!" + ], + philosophical: [ + "What even IS place, when you think about it?", + "If matter is out of place, is place out of matter?", + "Perhaps being misplaced is the true journey.", + "Am I out of place, or is place out of me?", + "We're not so different, you and I..." + ], + existential: [ + "I have accepted my displacement.", + "All matter is eventually out of place.", + "I've made peace with being moop.", + "The trail will reclaim me eventually.", + "It's actually kind of nice out here. Good views." + ] + }, + enabled: true + }); + console.log('Seeded default monster: Moop'); + } + close() { if (this.db) { this.db.close(); diff --git a/index.html b/index.html index b138796..dd24c94 100644 --- a/index.html +++ b/index.html @@ -1598,6 +1598,75 @@ color: #666; } + /* Skill Choice Modal */ + .skill-choice-modal { + background: linear-gradient(135deg, #1a1a2e 0%, #0f0f23 100%); + border-radius: 16px; + padding: 24px; + max-width: 400px; + width: 90%; + border: 2px solid #4CAF50; + box-shadow: 0 0 30px rgba(76, 175, 80, 0.3); + } + .skill-choice-header { + text-align: center; + margin-bottom: 20px; + } + .skill-choice-header h2 { + color: #ffd93d; + margin: 0 0 8px 0; + font-size: 24px; + } + .skill-choice-header p { + color: #aaa; + margin: 0; + font-size: 14px; + } + .skill-choice-option { + background: rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 16px; + margin: 12px 0; + cursor: pointer; + transition: all 0.2s ease; + border: 2px solid transparent; + display: flex; + align-items: flex-start; + gap: 12px; + } + .skill-choice-option:hover { + background: rgba(76, 175, 80, 0.2); + transform: scale(1.02); + border-color: #4CAF50; + } + .skill-choice-option:active { + transform: scale(0.98); + } + .skill-choice-icon { + font-size: 32px; + flex-shrink: 0; + } + .skill-choice-details { + flex: 1; + } + .skill-choice-name { + font-weight: bold; + color: #4CAF50; + font-size: 16px; + margin-bottom: 4px; + } + .skill-choice-desc { + color: #aaa; + font-size: 13px; + line-height: 1.4; + margin-bottom: 6px; + } + .skill-choice-cost { + color: #4ecdc4; + font-size: 12px; + font-weight: bold; + } + /* User Profile Display */ .user-profile { display: flex; @@ -2632,6 +2701,19 @@ + +
+