diff --git a/database.js b/database.js index cc9250b..d429a44 100644 --- a/database.js +++ b/database.js @@ -65,6 +65,8 @@ class HikeMapDB { this.db.exec(` CREATE TABLE IF NOT EXISTS rpg_stats ( user_id INTEGER PRIMARY KEY, + character_name TEXT, + race TEXT DEFAULT 'human', class TEXT NOT NULL DEFAULT 'trail_runner', level INTEGER DEFAULT 1, xp INTEGER DEFAULT 0, @@ -79,6 +81,33 @@ class HikeMapDB { ) `); + // Migration: Add character_name and race columns if they don't exist + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN character_name TEXT`); + } catch (e) { /* Column already exists */ } + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN race TEXT DEFAULT 'human'`); + } catch (e) { /* Column already exists */ } + + // Monster entourage table - stores monsters following the player + this.db.exec(` + CREATE TABLE IF NOT EXISTS monster_entourage ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + monster_type TEXT NOT NULL, + level INTEGER NOT NULL, + hp INTEGER NOT NULL, + max_hp INTEGER NOT NULL, + atk INTEGER NOT NULL, + def INTEGER NOT NULL, + position_lat REAL, + position_lng REAL, + spawn_time INTEGER NOT NULL, + last_dialogue_time INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + // Create indexes for performance this.db.exec(` CREATE INDEX IF NOT EXISTS idx_geocache_finds_user ON geocache_finds(user_id); @@ -86,6 +115,7 @@ class HikeMapDB { CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + CREATE INDEX IF NOT EXISTS idx_monster_entourage_user ON monster_entourage(user_id); `); } @@ -324,17 +354,60 @@ class HikeMapDB { // RPG Stats methods getRpgStats(userId) { const stmt = this.db.prepare(` - SELECT 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 FROM rpg_stats WHERE user_id = ? `); return stmt.get(userId); } + hasCharacter(userId) { + const stmt = this.db.prepare(` + SELECT 1 FROM rpg_stats WHERE user_id = ? AND character_name IS NOT NULL + `); + return !!stmt.get(userId); + } + + 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')) + ON CONFLICT(user_id) DO UPDATE SET + character_name = excluded.character_name, + race = excluded.race, + class = excluded.class, + level = excluded.level, + xp = excluded.xp, + hp = excluded.hp, + max_hp = excluded.max_hp, + mp = excluded.mp, + max_mp = excluded.max_mp, + atk = excluded.atk, + def = excluded.def, + updated_at = datetime('now') + `); + return stmt.run( + userId, + characterData.name, + characterData.race || 'human', + characterData.class || 'trail_runner', + characterData.level || 1, + characterData.xp || 0, + characterData.hp || 100, + characterData.maxHp || 100, + characterData.mp || 50, + characterData.maxMp || 50, + characterData.atk || 12, + characterData.def || 8 + ); + } + saveRpgStats(userId, stats) { const stmt = this.db.prepare(` - INSERT INTO rpg_stats (user_id, 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, 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), class = excluded.class, level = excluded.level, xp = excluded.xp, @@ -348,6 +421,8 @@ class HikeMapDB { `); return stmt.run( userId, + stats.name || null, + stats.race || null, stats.class || 'trail_runner', stats.level || 1, stats.xp || 0, @@ -360,6 +435,52 @@ class HikeMapDB { ); } + // Monster entourage methods + getMonsterEntourage(userId) { + const stmt = this.db.prepare(` + SELECT id, monster_type, level, hp, max_hp, atk, def, + position_lat, position_lng, spawn_time, last_dialogue_time + FROM monster_entourage WHERE user_id = ? + `); + return stmt.all(userId); + } + + saveMonsterEntourage(userId, monsters) { + const deleteStmt = this.db.prepare(`DELETE FROM monster_entourage WHERE user_id = ?`); + const insertStmt = this.db.prepare(` + INSERT INTO monster_entourage + (id, user_id, monster_type, level, hp, max_hp, atk, def, position_lat, position_lng, spawn_time, last_dialogue_time) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + const transaction = this.db.transaction(() => { + deleteStmt.run(userId); + for (const monster of monsters) { + insertStmt.run( + monster.id, + userId, + monster.type, + monster.level, + monster.hp, + monster.maxHp, + monster.atk, + monster.def, + monster.position?.lat || null, + monster.position?.lng || null, + monster.spawnTime, + monster.lastDialogueTime || 0 + ); + } + }); + + return transaction(); + } + + removeMonster(userId, monsterId) { + const stmt = this.db.prepare(`DELETE FROM monster_entourage WHERE user_id = ? AND id = ?`); + return stmt.run(userId, monsterId); + } + close() { if (this.db) { this.db.close(); diff --git a/index.html b/index.html index 1c62954..b138796 100644 --- a/index.html +++ b/index.html @@ -46,6 +46,14 @@ position: relative; z-index: 1; } + /* In nav mode, disable pointer events on trails so markers are easier to tap */ + body.nav-mode .leaflet-overlay-pane path.leaflet-interactive { + pointer-events: none; + } + /* But keep route highlight interactive for potential future use */ + body.nav-mode .leaflet-overlay-pane path.route-highlight { + pointer-events: auto; + } /* Ensure Leaflet doesn't block our popups */ .leaflet-container { z-index: 1 !important; @@ -443,6 +451,12 @@ border: none; font-size: 20px; cursor: pointer; + /* Larger tap target for mobile */ + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; } .geocache-marker:hover { transform: scale(1.2); @@ -452,6 +466,10 @@ box-shadow: 0 0 20px rgba(255, 167, 38, 0.8); border-radius: 50%; } + /* Increase tap area for geocache icon */ + .geocache-marker i { + padding: 10px; + } .geocache-dialog { position: fixed !important; top: 0 !important; @@ -1153,6 +1171,433 @@ cursor: pointer; color: #666; } + .auth-guest-divider { + display: flex; + align-items: center; + margin: 20px 0 15px 0; + color: #999; + font-size: 13px; + } + .auth-guest-divider::before, + .auth-guest-divider::after { + content: ''; + flex: 1; + height: 1px; + background: #ddd; + } + .auth-guest-divider span { + padding: 0 15px; + } + .auth-guest-btn { + width: 100%; + padding: 12px; + background: transparent; + color: #666; + border: 2px solid #ddd; + border-radius: 4px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + } + .auth-guest-btn:hover { + border-color: #999; + color: #333; + background: #f5f5f5; + } + /* Character Creator Modal */ + .char-creator-modal { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + padding: 0; + border-radius: 16px; + width: 95%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + color: white; + } + .char-creator-header { + background: linear-gradient(90deg, #4CAF50 0%, #8BC34A 100%); + padding: 20px; + text-align: center; + border-radius: 16px 16px 0 0; + } + .char-creator-header h2 { + margin: 0; + font-size: 24px; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); + } + .char-creator-header p { + margin: 5px 0 0 0; + font-size: 14px; + opacity: 0.9; + } + .char-creator-content { + padding: 20px; + } + .char-creator-step { + display: none; + } + .char-creator-step.active { + display: block; + } + .char-creator-section { + margin-bottom: 20px; + } + .char-creator-section h3 { + margin: 0 0 10px 0; + font-size: 16px; + color: #8BC34A; + } + .char-creator-input { + width: 100%; + padding: 12px; + border: 2px solid #333; + border-radius: 8px; + background: #0f0f23; + color: white; + font-size: 16px; + box-sizing: border-box; + } + .char-creator-input:focus { + border-color: #4CAF50; + outline: none; + } + .char-creator-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + } + .char-creator-option { + background: #0f0f23; + border: 2px solid #333; + border-radius: 12px; + padding: 15px; + cursor: pointer; + transition: all 0.2s; + text-align: center; + } + .char-creator-option:hover { + border-color: #4CAF50; + background: #1a1a2e; + } + .char-creator-option.selected { + border-color: #4CAF50; + background: rgba(76, 175, 80, 0.2); + } + .char-creator-option.disabled { + opacity: 0.5; + cursor: not-allowed; + } + .char-creator-option.disabled:hover { + border-color: #333; + background: #0f0f23; + } + .char-creator-option-icon { + font-size: 36px; + margin-bottom: 8px; + } + .char-creator-option-name { + font-weight: bold; + margin-bottom: 4px; + } + .char-creator-option-desc { + font-size: 12px; + color: #aaa; + } + .char-creator-option-badge { + display: inline-block; + background: #666; + color: #ccc; + font-size: 10px; + padding: 2px 8px; + border-radius: 10px; + margin-top: 5px; + } + .char-creator-stats { + display: flex; + justify-content: center; + gap: 15px; + margin-top: 8px; + font-size: 11px; + } + .char-creator-stat { + display: flex; + align-items: center; + gap: 3px; + } + .char-creator-stat.positive { color: #4CAF50; } + .char-creator-stat.negative { color: #f44336; } + .char-creator-stat.neutral { color: #888; } + .char-creator-preview { + background: #0f0f23; + border: 2px solid #333; + border-radius: 12px; + padding: 20px; + text-align: center; + margin-bottom: 20px; + } + .char-creator-preview-icon { + font-size: 48px; + margin-bottom: 10px; + } + .char-creator-preview-name { + font-size: 20px; + font-weight: bold; + color: #8BC34A; + } + .char-creator-preview-info { + font-size: 14px; + color: #aaa; + margin-top: 5px; + } + .char-creator-preview-stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-top: 15px; + text-align: left; + } + .char-creator-preview-stat { + display: flex; + justify-content: space-between; + padding: 8px 12px; + background: #1a1a2e; + border-radius: 6px; + } + .char-creator-preview-stat-label { + color: #888; + } + .char-creator-preview-stat-value { + font-weight: bold; + } + .char-creator-buttons { + display: flex; + gap: 10px; + margin-top: 20px; + } + .char-creator-btn { + flex: 1; + padding: 14px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + } + .char-creator-btn-back { + background: #333; + color: white; + } + .char-creator-btn-back:hover { + background: #444; + } + .char-creator-btn-next { + background: linear-gradient(90deg, #4CAF50 0%, #8BC34A 100%); + color: white; + } + .char-creator-btn-next:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4); + } + .char-creator-btn-next:disabled { + background: #333; + cursor: not-allowed; + transform: none; + box-shadow: none; + } + .char-step-indicator { + display: flex; + justify-content: center; + gap: 8px; + padding: 15px 0; + background: rgba(0,0,0,0.2); + } + .char-step-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #333; + transition: all 0.2s; + } + .char-step-dot.active { + background: #4CAF50; + transform: scale(1.2); + } + .char-step-dot.completed { + background: #8BC34A; + } + + /* Character Sheet Modal */ + .char-sheet-modal { + background: linear-gradient(135deg, #1a1a2e 0%, #0f0f23 100%); + border-radius: 20px; + padding: 0; + width: 90%; + max-width: 400px; + max-height: 85vh; + overflow-y: auto; + position: relative; + border: 2px solid #4CAF50; + box-shadow: 0 20px 60px rgba(0,0,0,0.5); + } + .char-sheet-close { + position: absolute; + top: 15px; + right: 15px; + background: none; + border: none; + color: #aaa; + font-size: 28px; + cursor: pointer; + z-index: 10; + transition: color 0.2s; + } + .char-sheet-close:hover { + color: #fff; + } + .char-sheet-header { + background: linear-gradient(135deg, #4CAF50, #8BC34A); + padding: 25px 20px; + text-align: center; + border-radius: 18px 18px 0 0; + } + .char-sheet-icon { + font-size: 48px; + margin-bottom: 10px; + } + .char-sheet-name { + font-size: 22px; + font-weight: bold; + color: #fff; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); + } + .char-sheet-info { + font-size: 14px; + color: rgba(255,255,255,0.9); + margin-top: 5px; + } + .char-sheet-content { + padding: 20px; + } + .char-sheet-section { + background: rgba(255,255,255,0.05); + border-radius: 12px; + padding: 15px; + margin-bottom: 15px; + } + .char-sheet-section h3 { + color: #8BC34A; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; + margin: 0 0 12px 0; + padding-bottom: 8px; + border-bottom: 1px solid rgba(139,195,74,0.3); + } + .char-sheet-stat-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + } + .char-sheet-stat { + display: flex; + flex-direction: column; + gap: 4px; + } + .char-sheet-stat .stat-label { + font-size: 12px; + color: #aaa; + } + .char-sheet-stat .stat-value { + font-size: 16px; + font-weight: bold; + color: #fff; + } + .char-sheet-stat .stat-bar { + height: 8px; + background: rgba(0,0,0,0.3); + border-radius: 4px; + overflow: hidden; + } + .char-sheet-stat .stat-bar.hp-bar .stat-fill { + background: linear-gradient(90deg, #ff6b6b, #ee5a5a); + } + .char-sheet-stat .stat-bar.mp-bar .stat-fill { + background: linear-gradient(90deg, #4ecdc4, #45b7aa); + } + .char-sheet-stat .stat-fill { + height: 100%; + transition: width 0.3s ease; + } + .char-sheet-xp .xp-bar-container { + height: 12px; + background: rgba(0,0,0,0.3); + border-radius: 6px; + overflow: hidden; + margin-bottom: 8px; + } + .char-sheet-xp .xp-bar-fill { + height: 100%; + background: linear-gradient(90deg, #ffd93d, #f0c419); + transition: width 0.3s ease; + } + .char-sheet-xp .xp-text { + font-size: 14px; + font-weight: bold; + color: #ffd93d; + text-align: center; + } + .char-sheet-xp .xp-next { + font-size: 12px; + color: #888; + text-align: center; + margin-top: 4px; + } + .char-sheet-skill { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 10px; + background: rgba(0,0,0,0.2); + border-radius: 8px; + margin-bottom: 8px; + } + .char-sheet-skill:last-child { + margin-bottom: 0; + } + .char-sheet-skill .skill-icon { + font-size: 24px; + min-width: 30px; + text-align: center; + } + .char-sheet-skill .skill-info { + flex: 1; + } + .char-sheet-skill .skill-name { + font-size: 14px; + font-weight: bold; + color: #fff; + } + .char-sheet-skill .skill-desc { + font-size: 12px; + color: #aaa; + margin-top: 2px; + } + .char-sheet-skill .skill-cost { + font-size: 11px; + color: #4ecdc4; + margin-top: 4px; + } + .char-sheet-skill.locked { + opacity: 0.5; + } + .char-sheet-skill.locked .skill-name { + color: #666; + } + /* User Profile Display */ .user-profile { display: flex; @@ -1383,13 +1828,26 @@ position: relative; cursor: pointer; transition: transform 0.2s; + /* Large tap target for mobile */ + display: flex; + align-items: center; + justify-content: center; + width: 70px; + height: 70px; + /* Semi-transparent background for tap area */ + background: radial-gradient(circle, rgba(255,100,100,0.25) 0%, rgba(255,100,100,0) 70%); + border-radius: 50%; + } + .monster-marker:hover { + transform: scale(1.2); } - .monster-marker:hover { - transform: scale(1.3); + .monster-marker:active { + transform: scale(0.95); + background: radial-gradient(circle, rgba(255,100,100,0.4) 0%, rgba(255,100,100,0) 70%); } .monster-icon { - font-size: 36px; - filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.5)); + font-size: 44px; + filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.6)); animation: monster-bob 2s ease-in-out infinite; } @keyframes monster-bob { @@ -1784,7 +2242,7 @@
@@ -2059,6 +2523,115 @@ + + + + + +