mirror of
https://github.com/blackboxprogramming/blackroad-roadworld.git
synced 2026-03-18 06:04:01 -05:00
Add enhanced features for RoadWorld v4.0
This release adds 6 major new features to enhance gameplay: - Achievements System: 20+ badges across 5 categories with XP rewards - Daily Missions: Regenerating daily challenges with varied objectives - Weather Effects: Atmospheric particles (rain, snow, fog) and time-of-day - Sound Effects: Procedural audio feedback using Web Audio API - Mini-map: Navigation overview with compass and player tracking - Statistics Dashboard: Comprehensive player stats with tabbed interface New files: - achievementsManager.js: Badge tracking and unlock system - missionsManager.js: Daily mission generation and progress - weatherEffects.js: Canvas-based weather particle system - soundManager.js: Procedural sound generation - minimap.js: Synchronized navigation overlay - statisticsPanel.js: Multi-tab stats panel Updates: - main.js: Integration of all new features - main.css: 750+ lines of new component styles - index.html: New control buttons and HUD elements
This commit is contained in:
184
STATUS_V4.md
Normal file
184
STATUS_V4.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# RoadWorld v4.0 - Enhanced Features Release
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Version 4.0 brings significant enhancements to the RoadWorld gaming experience with six major new features that increase engagement, provide atmospheric immersion, and offer comprehensive player progress tracking.
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
### 1. Achievements System
|
||||||
|
**File:** `src/js/achievementsManager.js`
|
||||||
|
|
||||||
|
A comprehensive badge and accomplishment tracking system with 20+ achievements across 5 categories:
|
||||||
|
|
||||||
|
- **Explorer Achievements**: First Steps, Marathon Runner, World Traveler, Globe Trotter
|
||||||
|
- **Collector Achievements**: First Find, Treasure Hunter, Hoarder, Star/Gem/Trophy/Key collectors
|
||||||
|
- **Level Achievements**: Rising Star (L5), Seasoned Explorer (L10), Elite Adventurer (L25), Master Explorer (L50), Legend (L100)
|
||||||
|
- **Time Achievements**: Dedicated Player (1hr), Time Traveler (10hr), Eternal Explorer (100hr)
|
||||||
|
- **Special Achievements**: Night Owl, Early Bird, Speed Demon
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Rarity tiers: Common, Rare, Epic, Legendary
|
||||||
|
- XP rewards for each achievement (10-5000 XP)
|
||||||
|
- Persistent storage across sessions
|
||||||
|
- Real-time unlock notifications with animations
|
||||||
|
- Sound effects on unlock
|
||||||
|
|
||||||
|
### 2. Daily Missions System
|
||||||
|
**File:** `src/js/missionsManager.js`
|
||||||
|
|
||||||
|
Daily challenges that reset at midnight with varied objectives:
|
||||||
|
|
||||||
|
**Mission Types:**
|
||||||
|
- Distance missions (travel X meters/kilometers)
|
||||||
|
- Collection missions (collect stars, gems, items)
|
||||||
|
- XP earning missions
|
||||||
|
- Exploration missions
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- 3 missions generated daily with varying difficulty
|
||||||
|
- Progress tracking with visual progress bars
|
||||||
|
- Claimable XP rewards upon completion
|
||||||
|
- Reset timer countdown display
|
||||||
|
- Persistent mission state
|
||||||
|
|
||||||
|
### 3. Weather Effects System
|
||||||
|
**File:** `src/js/weatherEffects.js`
|
||||||
|
|
||||||
|
Atmospheric visual effects that enhance immersion:
|
||||||
|
|
||||||
|
**Weather Types:**
|
||||||
|
- Clear
|
||||||
|
- Cloudy
|
||||||
|
- Rain / Storm (with lightning)
|
||||||
|
- Snow
|
||||||
|
- Fog
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Canvas-based particle rendering for rain/snow/fog
|
||||||
|
- Time-of-day tinting (dawn, day, dusk, night)
|
||||||
|
- Lightning flash effects during storms
|
||||||
|
- Weather overlays for atmosphere
|
||||||
|
- Toggle on/off via toolbar button
|
||||||
|
|
||||||
|
### 4. Sound Effects System
|
||||||
|
**File:** `src/js/soundManager.js`
|
||||||
|
|
||||||
|
Audio feedback using Web Audio API (no external files needed):
|
||||||
|
|
||||||
|
**Sound Effects:**
|
||||||
|
- Collection sounds (different for each rarity)
|
||||||
|
- Level up fanfare
|
||||||
|
- Achievement unlock jingle
|
||||||
|
- Mission complete sound
|
||||||
|
- Movement feedback
|
||||||
|
- Click/UI sounds
|
||||||
|
- Ambient exploration pads
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Procedurally generated tones
|
||||||
|
- Volume control
|
||||||
|
- Mute/unmute toggle
|
||||||
|
- Initializes on first user interaction
|
||||||
|
|
||||||
|
### 5. Mini-map Navigation
|
||||||
|
**File:** `src/js/minimap.js`
|
||||||
|
|
||||||
|
A navigation overview panel in the corner:
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Synchronized with main map position
|
||||||
|
- Dark theme for visibility
|
||||||
|
- Compass with north indicator
|
||||||
|
- Player position marker
|
||||||
|
- Coordinate display
|
||||||
|
- Expandable view
|
||||||
|
- Click-to-navigate functionality
|
||||||
|
- Collapsible toggle
|
||||||
|
|
||||||
|
### 6. Statistics Dashboard
|
||||||
|
**File:** `src/js/statisticsPanel.js`
|
||||||
|
|
||||||
|
Comprehensive player stats panel with tabbed interface:
|
||||||
|
|
||||||
|
**Tabs:**
|
||||||
|
1. **Overview**: Profile card, level/XP progress, stat cards (distance, items, achievements, playtime, locations, missions)
|
||||||
|
2. **Achievements**: Progress bar, categorized achievement list with unlock status
|
||||||
|
3. **Missions**: Daily mission list with progress, claim buttons, reset timer
|
||||||
|
4. **Inventory**: Item counts, recent finds with rarity display
|
||||||
|
|
||||||
|
## UI Updates
|
||||||
|
|
||||||
|
### New Control Buttons
|
||||||
|
Added to the main toolbar:
|
||||||
|
- **Statistics** (📊): Opens the full statistics dashboard
|
||||||
|
- **Weather** (🌤️): Toggles weather effects on/off
|
||||||
|
- **Sound** (🔊/🔇): Toggles sound effects on/off
|
||||||
|
|
||||||
|
### Enhanced Game HUD
|
||||||
|
Added indicators for:
|
||||||
|
- Current mission progress (X/3 missions)
|
||||||
|
- Achievement count
|
||||||
|
|
||||||
|
### New Notifications
|
||||||
|
- Collection notifications with rarity styling
|
||||||
|
- Achievement unlock banners
|
||||||
|
- Level up celebrations
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- No new external dependencies
|
||||||
|
- Uses existing MapLibre GL for minimap
|
||||||
|
- Web Audio API for sound generation
|
||||||
|
- Canvas API for weather particles
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
New localStorage keys:
|
||||||
|
- `achievements`: Array of unlocked achievement IDs
|
||||||
|
- `dailyMissions`: Current missions, progress, and reset tracking
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Weather effects use requestAnimationFrame for smooth rendering
|
||||||
|
- Sound generation is lightweight (no audio file loading)
|
||||||
|
- Minimap uses simplified tile layer
|
||||||
|
|
||||||
|
## File Changes Summary
|
||||||
|
|
||||||
|
### New Files (6)
|
||||||
|
```
|
||||||
|
src/js/achievementsManager.js - Achievement tracking
|
||||||
|
src/js/missionsManager.js - Daily missions
|
||||||
|
src/js/weatherEffects.js - Weather particle system
|
||||||
|
src/js/soundManager.js - Audio feedback
|
||||||
|
src/js/minimap.js - Navigation overview
|
||||||
|
src/js/statisticsPanel.js - Stats dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modified Files (3)
|
||||||
|
```
|
||||||
|
src/js/main.js - Integration of all new features
|
||||||
|
src/css/main.css - 750+ lines of new styles
|
||||||
|
public/index.html - New control buttons, HUD updates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Game Mode**: Click the 🎮 button to activate game mode
|
||||||
|
2. **View Stats**: Click 📊 to open the statistics dashboard
|
||||||
|
3. **Weather**: Weather effects start automatically; toggle with 🌤️
|
||||||
|
4. **Sound**: Sounds are on by default; toggle with 🔊
|
||||||
|
5. **Minimap**: Visible in bottom-right; click to navigate, use controls to expand/collapse
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v4.0**: Enhanced Features (Achievements, Missions, Weather, Sound, Minimap, Stats)
|
||||||
|
- **v3.0**: Open World Game Mode (Player avatar, collectibles, XP/levels)
|
||||||
|
- **v2.0**: Advanced Features (3D buildings, markers, measurements, URL sharing)
|
||||||
|
- **v1.0**: Initial Release (Interactive globe explorer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Build Date:** 2025-12-28
|
||||||
|
**Total Lines Added:** ~2,500
|
||||||
|
**New Features:** 6
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>BlackRoad Earth | RoadWorld Module</title>
|
<title>BlackRoad Earth | RoadWorld v4.0</title>
|
||||||
|
|
||||||
<!-- MapLibre GL -->
|
<!-- MapLibre GL -->
|
||||||
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
|
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
|
||||||
@@ -156,6 +156,10 @@
|
|||||||
<button class="ctrl-btn" id="btn-measure" title="Measure Distance">📏</button>
|
<button class="ctrl-btn" id="btn-measure" title="Measure Distance">📏</button>
|
||||||
<button class="ctrl-btn" id="btn-share" title="Share Location">🔗</button>
|
<button class="ctrl-btn" id="btn-share" title="Share Location">🔗</button>
|
||||||
<button class="ctrl-btn" id="btn-tools" title="More Tools">🛠️</button>
|
<button class="ctrl-btn" id="btn-tools" title="More Tools">🛠️</button>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<button class="ctrl-btn" id="btn-stats" title="Statistics">📊</button>
|
||||||
|
<button class="ctrl-btn active" id="btn-weather" title="Weather Effects">🌤️</button>
|
||||||
|
<button class="ctrl-btn active" id="btn-sound" title="Sound">🔊</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Instructions -->
|
<!-- Instructions -->
|
||||||
@@ -275,6 +279,8 @@
|
|||||||
<div style="font-size: 11px; line-height: 1.6;">
|
<div style="font-size: 11px; line-height: 1.6;">
|
||||||
<div>🚶 <span id="hud-distance">0 m</span></div>
|
<div>🚶 <span id="hud-distance">0 m</span></div>
|
||||||
<div>✨ <span id="hud-collected">0</span> items</div>
|
<div>✨ <span id="hud-collected">0</span> items</div>
|
||||||
|
<div>🎯 <span id="hud-missions">0</span>/3 missions</div>
|
||||||
|
<div>🏆 <span id="hud-achievements">0</span> achievements</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
751
src/css/main.css
751
src/css/main.css
@@ -1274,3 +1274,754 @@ body {
|
|||||||
0%, 100% { box-shadow: 0 0 20px rgba(0, 212, 255, 0.6); }
|
0%, 100% { box-shadow: 0 0 20px rgba(0, 212, 255, 0.6); }
|
||||||
50% { box-shadow: 0 0 40px rgba(123, 47, 247, 0.8); }
|
50% { box-shadow: 0 0 40px rgba(123, 47, 247, 0.8); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== ENHANCED FEATURES v4.0 ==================== */
|
||||||
|
|
||||||
|
/* Weather Effects Container */
|
||||||
|
.weather-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weather-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: background 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightning-flash {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
z-index: 51;
|
||||||
|
animation: flash 0.1s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash {
|
||||||
|
0%, 100% { opacity: 0; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Time of Day Tints */
|
||||||
|
body[data-time="dawn"] { }
|
||||||
|
body[data-time="day"] { }
|
||||||
|
body[data-time="dusk"] { }
|
||||||
|
body[data-time="night"] { }
|
||||||
|
|
||||||
|
/* Minimap */
|
||||||
|
.minimap-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 180px;
|
||||||
|
right: 20px;
|
||||||
|
width: 200px;
|
||||||
|
background: rgba(10, 15, 30, 0.95);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 100;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-container.expanded {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-container.expanded .minimap {
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-title {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0, 212, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-btn:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-compass {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(0, 212, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-needle {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 16px;
|
||||||
|
background: linear-gradient(to bottom, #ff4444, #fff);
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-label {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 7px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-label.north { top: 2px; left: 50%; transform: translateX(-50%); color: #ff4444; }
|
||||||
|
.compass-label.south { bottom: 2px; left: 50%; transform: translateX(-50%); }
|
||||||
|
.compass-label.east { right: 2px; top: 50%; transform: translateY(-50%); }
|
||||||
|
.compass-label.west { left: 2px; top: 50%; transform: translateY(-50%); }
|
||||||
|
|
||||||
|
.minimap-player-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #00d4ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 10px #00d4ff;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-player-marker .player-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #00d4ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 212, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-player-marker .player-direction {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-bottom: 8px solid #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-coords {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statistics Panel */
|
||||||
|
.statistics-panel {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 500px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: rgba(10, 15, 30, 0.98);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.4);
|
||||||
|
border-radius: 16px;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 50px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-family: 'Exo 2', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tab:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tab.active {
|
||||||
|
color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.15);
|
||||||
|
border-bottom: 2px solid #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tab-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: calc(80vh - 120px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile Section */
|
||||||
|
.stats-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(0, 212, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-name {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-level {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #FFD700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-xp-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-xp-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #7b2ff7);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-xp-text {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Grid */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(0, 212, 255, 0.05);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-color: rgba(0, 212, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-value {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-label {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achievements */
|
||||||
|
.achievements-progress {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-progress-bar {
|
||||||
|
height: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #7b2ff7, #FFD700);
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-progress-text {
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-category {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-category h4 {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-item.unlocked {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-item.locked {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-item.common { border-left-color: #fff; }
|
||||||
|
.achievement-item.rare { border-left-color: #00d4ff; }
|
||||||
|
.achievement-item.epic { border-left-color: #7b2ff7; }
|
||||||
|
.achievement-item.legendary { border-left-color: #FFD700; }
|
||||||
|
|
||||||
|
.achievement-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-name {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-reward {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #FFD700;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Missions */
|
||||||
|
.missions-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missions-timer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missions-claimable {
|
||||||
|
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||||
|
color: #000;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missions-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-item.completed {
|
||||||
|
border-color: rgba(0, 255, 100, 0.4);
|
||||||
|
background: rgba(0, 255, 100, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-item.claimed {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-name {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-progress-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #7b2ff7);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-progress-text {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-reward {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inventory */
|
||||||
|
.inventory-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-item-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(0, 212, 255, 0.05);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-count {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-label {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-total {
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-items {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-item.common { border: 1px solid rgba(255, 255, 255, 0.3); }
|
||||||
|
.recent-item.rare { border: 1px solid rgba(0, 212, 255, 0.5); }
|
||||||
|
.recent-item.epic { border: 1px solid rgba(123, 47, 247, 0.5); }
|
||||||
|
.recent-item.legendary { border: 1px solid rgba(255, 215, 0, 0.5); }
|
||||||
|
|
||||||
|
.recent-item-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-item-rarity {
|
||||||
|
font-size: 9px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achievement Notification */
|
||||||
|
.achievement-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
right: -400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: rgba(10, 15, 30, 0.98);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
transition: right 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
z-index: 1002;
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-notification.show {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-notification.common { border-left-color: #fff; }
|
||||||
|
.achievement-notification.rare { border-left-color: #00d4ff; }
|
||||||
|
.achievement-notification.epic { border-left-color: #7b2ff7; }
|
||||||
|
.achievement-notification.legendary {
|
||||||
|
border-left-color: #FFD700;
|
||||||
|
animation: legendaryGlow 1s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes legendaryGlow {
|
||||||
|
from { box-shadow: 0 8px 32px rgba(255, 215, 0, 0.2); }
|
||||||
|
to { box-shadow: 0 8px 48px rgba(255, 215, 0, 0.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-unlock-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-unlock-title {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-unlock-name {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #00d4ff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-unlock-xp {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #FFD700;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weather Info Overlay */
|
||||||
|
.weather-info {
|
||||||
|
position: fixed;
|
||||||
|
top: 75px;
|
||||||
|
right: 300px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(10, 15, 30, 0.9);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.minimap-container {
|
||||||
|
width: 150px;
|
||||||
|
bottom: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-wrapper {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-panel {
|
||||||
|
width: 95vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
333
src/js/achievementsManager.js
Normal file
333
src/js/achievementsManager.js
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
// Achievements System for RoadWorld
|
||||||
|
// Tracks player accomplishments and unlocks badges
|
||||||
|
|
||||||
|
export class AchievementsManager {
|
||||||
|
constructor(gameEngine, storageManager) {
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
// Define all achievements
|
||||||
|
this.achievements = this.defineAchievements();
|
||||||
|
|
||||||
|
// Load unlocked achievements
|
||||||
|
this.unlockedAchievements = this.loadUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineAchievements() {
|
||||||
|
return {
|
||||||
|
// Explorer Achievements
|
||||||
|
first_steps: {
|
||||||
|
id: 'first_steps',
|
||||||
|
name: 'First Steps',
|
||||||
|
description: 'Move for the first time',
|
||||||
|
icon: '🚶',
|
||||||
|
category: 'explorer',
|
||||||
|
rarity: 'common',
|
||||||
|
condition: (player) => player.stats.distanceTraveled > 0,
|
||||||
|
xpReward: 10
|
||||||
|
},
|
||||||
|
marathon_runner: {
|
||||||
|
id: 'marathon_runner',
|
||||||
|
name: 'Marathon Runner',
|
||||||
|
description: 'Travel 42.195 kilometers',
|
||||||
|
icon: '🏃',
|
||||||
|
category: 'explorer',
|
||||||
|
rarity: 'epic',
|
||||||
|
condition: (player) => player.stats.distanceTraveled >= 42195,
|
||||||
|
xpReward: 500
|
||||||
|
},
|
||||||
|
world_traveler: {
|
||||||
|
id: 'world_traveler',
|
||||||
|
name: 'World Traveler',
|
||||||
|
description: 'Travel 100 kilometers',
|
||||||
|
icon: '🌍',
|
||||||
|
category: 'explorer',
|
||||||
|
rarity: 'legendary',
|
||||||
|
condition: (player) => player.stats.distanceTraveled >= 100000,
|
||||||
|
xpReward: 1000
|
||||||
|
},
|
||||||
|
globe_trotter: {
|
||||||
|
id: 'globe_trotter',
|
||||||
|
name: 'Globe Trotter',
|
||||||
|
description: 'Travel 1000 kilometers',
|
||||||
|
icon: '✈️',
|
||||||
|
category: 'explorer',
|
||||||
|
rarity: 'legendary',
|
||||||
|
condition: (player) => player.stats.distanceTraveled >= 1000000,
|
||||||
|
xpReward: 5000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Collector Achievements
|
||||||
|
first_find: {
|
||||||
|
id: 'first_find',
|
||||||
|
name: 'First Find',
|
||||||
|
description: 'Collect your first item',
|
||||||
|
icon: '✨',
|
||||||
|
category: 'collector',
|
||||||
|
rarity: 'common',
|
||||||
|
condition: (player) => player.stats.itemsCollected >= 1,
|
||||||
|
xpReward: 10
|
||||||
|
},
|
||||||
|
treasure_hunter: {
|
||||||
|
id: 'treasure_hunter',
|
||||||
|
name: 'Treasure Hunter',
|
||||||
|
description: 'Collect 50 items',
|
||||||
|
icon: '🔍',
|
||||||
|
category: 'collector',
|
||||||
|
rarity: 'rare',
|
||||||
|
condition: (player) => player.stats.itemsCollected >= 50,
|
||||||
|
xpReward: 100
|
||||||
|
},
|
||||||
|
hoarder: {
|
||||||
|
id: 'hoarder',
|
||||||
|
name: 'Hoarder',
|
||||||
|
description: 'Collect 500 items',
|
||||||
|
icon: '💰',
|
||||||
|
category: 'collector',
|
||||||
|
rarity: 'epic',
|
||||||
|
condition: (player) => player.stats.itemsCollected >= 500,
|
||||||
|
xpReward: 500
|
||||||
|
},
|
||||||
|
star_collector: {
|
||||||
|
id: 'star_collector',
|
||||||
|
name: 'Star Collector',
|
||||||
|
description: 'Collect 100 stars',
|
||||||
|
icon: '⭐',
|
||||||
|
category: 'collector',
|
||||||
|
rarity: 'rare',
|
||||||
|
condition: (player) => (player.inventory.stars || 0) >= 100,
|
||||||
|
xpReward: 200
|
||||||
|
},
|
||||||
|
gem_enthusiast: {
|
||||||
|
id: 'gem_enthusiast',
|
||||||
|
name: 'Gem Enthusiast',
|
||||||
|
description: 'Collect 50 gems',
|
||||||
|
icon: '💎',
|
||||||
|
category: 'collector',
|
||||||
|
rarity: 'epic',
|
||||||
|
condition: (player) => (player.inventory.gems || 0) >= 50,
|
||||||
|
xpReward: 300
|
||||||
|
},
|
||||||
|
trophy_master: {
|
||||||
|
id: 'trophy_master',
|
||||||
|
name: 'Trophy Master',
|
||||||
|
description: 'Collect 25 trophies',
|
||||||
|
icon: '🏆',
|
||||||
|
category: 'collector',
|
||||||
|
rarity: 'epic',
|
||||||
|
condition: (player) => (player.inventory.trophies || 0) >= 25,
|
||||||
|
xpReward: 400
|
||||||
|
},
|
||||||
|
key_keeper: {
|
||||||
|
id: 'key_keeper',
|
||||||
|
name: 'Key Keeper',
|
||||||
|
description: 'Collect 10 legendary keys',
|
||||||
|
icon: '🗝️',
|
||||||
|
category: 'collector',
|
||||||
|
rarity: 'legendary',
|
||||||
|
condition: (player) => (player.inventory.keys || 0) >= 10,
|
||||||
|
xpReward: 1000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Level Achievements
|
||||||
|
level_5: {
|
||||||
|
id: 'level_5',
|
||||||
|
name: 'Rising Star',
|
||||||
|
description: 'Reach level 5',
|
||||||
|
icon: '🌟',
|
||||||
|
category: 'level',
|
||||||
|
rarity: 'common',
|
||||||
|
condition: (player) => player.level >= 5,
|
||||||
|
xpReward: 50
|
||||||
|
},
|
||||||
|
level_10: {
|
||||||
|
id: 'level_10',
|
||||||
|
name: 'Seasoned Explorer',
|
||||||
|
description: 'Reach level 10',
|
||||||
|
icon: '🎖️',
|
||||||
|
category: 'level',
|
||||||
|
rarity: 'rare',
|
||||||
|
condition: (player) => player.level >= 10,
|
||||||
|
xpReward: 100
|
||||||
|
},
|
||||||
|
level_25: {
|
||||||
|
id: 'level_25',
|
||||||
|
name: 'Elite Adventurer',
|
||||||
|
description: 'Reach level 25',
|
||||||
|
icon: '🏅',
|
||||||
|
category: 'level',
|
||||||
|
rarity: 'epic',
|
||||||
|
condition: (player) => player.level >= 25,
|
||||||
|
xpReward: 500
|
||||||
|
},
|
||||||
|
level_50: {
|
||||||
|
id: 'level_50',
|
||||||
|
name: 'Master Explorer',
|
||||||
|
description: 'Reach level 50',
|
||||||
|
icon: '👑',
|
||||||
|
category: 'level',
|
||||||
|
rarity: 'legendary',
|
||||||
|
condition: (player) => player.level >= 50,
|
||||||
|
xpReward: 1000
|
||||||
|
},
|
||||||
|
level_100: {
|
||||||
|
id: 'level_100',
|
||||||
|
name: 'Legend',
|
||||||
|
description: 'Reach level 100',
|
||||||
|
icon: '🌠',
|
||||||
|
category: 'level',
|
||||||
|
rarity: 'legendary',
|
||||||
|
condition: (player) => player.level >= 100,
|
||||||
|
xpReward: 5000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Time Achievements
|
||||||
|
dedicated_player: {
|
||||||
|
id: 'dedicated_player',
|
||||||
|
name: 'Dedicated Player',
|
||||||
|
description: 'Play for 1 hour total',
|
||||||
|
icon: '⏰',
|
||||||
|
category: 'time',
|
||||||
|
rarity: 'common',
|
||||||
|
condition: (player) => player.stats.playTime >= 3600000,
|
||||||
|
xpReward: 50
|
||||||
|
},
|
||||||
|
time_traveler: {
|
||||||
|
id: 'time_traveler',
|
||||||
|
name: 'Time Traveler',
|
||||||
|
description: 'Play for 10 hours total',
|
||||||
|
icon: '⌛',
|
||||||
|
category: 'time',
|
||||||
|
rarity: 'rare',
|
||||||
|
condition: (player) => player.stats.playTime >= 36000000,
|
||||||
|
xpReward: 200
|
||||||
|
},
|
||||||
|
eternal_explorer: {
|
||||||
|
id: 'eternal_explorer',
|
||||||
|
name: 'Eternal Explorer',
|
||||||
|
description: 'Play for 100 hours total',
|
||||||
|
icon: '🕰️',
|
||||||
|
category: 'time',
|
||||||
|
rarity: 'legendary',
|
||||||
|
condition: (player) => player.stats.playTime >= 360000000,
|
||||||
|
xpReward: 1000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Special Achievements
|
||||||
|
night_owl: {
|
||||||
|
id: 'night_owl',
|
||||||
|
name: 'Night Owl',
|
||||||
|
description: 'Play between midnight and 5 AM',
|
||||||
|
icon: '🦉',
|
||||||
|
category: 'special',
|
||||||
|
rarity: 'rare',
|
||||||
|
condition: () => {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
return hour >= 0 && hour < 5;
|
||||||
|
},
|
||||||
|
xpReward: 100
|
||||||
|
},
|
||||||
|
early_bird: {
|
||||||
|
id: 'early_bird',
|
||||||
|
name: 'Early Bird',
|
||||||
|
description: 'Play between 5 AM and 7 AM',
|
||||||
|
icon: '🐦',
|
||||||
|
category: 'special',
|
||||||
|
rarity: 'rare',
|
||||||
|
condition: () => {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
return hour >= 5 && hour < 7;
|
||||||
|
},
|
||||||
|
xpReward: 100
|
||||||
|
},
|
||||||
|
speed_demon: {
|
||||||
|
id: 'speed_demon',
|
||||||
|
name: 'Speed Demon',
|
||||||
|
description: 'Travel 1km in under 2 minutes',
|
||||||
|
icon: '💨',
|
||||||
|
category: 'special',
|
||||||
|
rarity: 'epic',
|
||||||
|
condition: (player) => player.stats.fastestKm && player.stats.fastestKm < 120000,
|
||||||
|
xpReward: 300
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUnlocked() {
|
||||||
|
const saved = this.storageManager.data.achievements || [];
|
||||||
|
return new Set(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUnlocked() {
|
||||||
|
this.storageManager.data.achievements = Array.from(this.unlockedAchievements);
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAchievements(player) {
|
||||||
|
const newlyUnlocked = [];
|
||||||
|
|
||||||
|
for (const [id, achievement] of Object.entries(this.achievements)) {
|
||||||
|
if (this.unlockedAchievements.has(id)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (achievement.condition(player)) {
|
||||||
|
this.unlockAchievement(id, achievement);
|
||||||
|
newlyUnlocked.push(achievement);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Condition failed, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newlyUnlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockAchievement(id, achievement) {
|
||||||
|
this.unlockedAchievements.add(id);
|
||||||
|
this.saveUnlocked();
|
||||||
|
|
||||||
|
// Award XP
|
||||||
|
if (this.gameEngine && achievement.xpReward) {
|
||||||
|
this.gameEngine.addXP(achievement.xpReward, `achievement: ${achievement.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🏆 Achievement Unlocked: ${achievement.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAchievementProgress() {
|
||||||
|
const total = Object.keys(this.achievements).length;
|
||||||
|
const unlocked = this.unlockedAchievements.size;
|
||||||
|
return {
|
||||||
|
unlocked,
|
||||||
|
total,
|
||||||
|
percentage: Math.round((unlocked / total) * 100)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllAchievements() {
|
||||||
|
return Object.entries(this.achievements).map(([id, achievement]) => ({
|
||||||
|
...achievement,
|
||||||
|
unlocked: this.unlockedAchievements.has(id)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAchievementsByCategory(category) {
|
||||||
|
return this.getAllAchievements().filter(a => a.category === category);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentUnlocks(count = 5) {
|
||||||
|
// Get recently unlocked achievements
|
||||||
|
const unlocked = this.getAllAchievements().filter(a => a.unlocked);
|
||||||
|
return unlocked.slice(-count);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRarityColor(rarity) {
|
||||||
|
const colors = {
|
||||||
|
common: '#FFFFFF',
|
||||||
|
rare: '#00d4ff',
|
||||||
|
epic: '#7b2ff7',
|
||||||
|
legendary: '#FFD700'
|
||||||
|
};
|
||||||
|
return colors[rarity] || colors.common;
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/js/main.js
222
src/js/main.js
@@ -10,6 +10,14 @@ import { GameEngine } from './gameEngine.js';
|
|||||||
import { PlayerAvatar } from './playerAvatar.js';
|
import { PlayerAvatar } from './playerAvatar.js';
|
||||||
import { CollectiblesRenderer } from './collectiblesRenderer.js';
|
import { CollectiblesRenderer } from './collectiblesRenderer.js';
|
||||||
|
|
||||||
|
// New enhanced features
|
||||||
|
import { AchievementsManager } from './achievementsManager.js';
|
||||||
|
import { MissionsManager } from './missionsManager.js';
|
||||||
|
import { WeatherEffects } from './weatherEffects.js';
|
||||||
|
import { SoundManager } from './soundManager.js';
|
||||||
|
import { Minimap } from './minimap.js';
|
||||||
|
import { StatisticsPanel } from './statisticsPanel.js';
|
||||||
|
|
||||||
class RoadWorldApp {
|
class RoadWorldApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.mapManager = null;
|
this.mapManager = null;
|
||||||
@@ -26,6 +34,14 @@ class RoadWorldApp {
|
|||||||
this.playerAvatar = null;
|
this.playerAvatar = null;
|
||||||
this.collectiblesRenderer = null;
|
this.collectiblesRenderer = null;
|
||||||
this.gameActive = false;
|
this.gameActive = false;
|
||||||
|
|
||||||
|
// Enhanced features (v4.0)
|
||||||
|
this.achievementsManager = null;
|
||||||
|
this.missionsManager = null;
|
||||||
|
this.weatherEffects = null;
|
||||||
|
this.soundManager = null;
|
||||||
|
this.minimap = null;
|
||||||
|
this.statisticsPanel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -80,7 +96,79 @@ class RoadWorldApp {
|
|||||||
// Setup game toggle
|
// Setup game toggle
|
||||||
this.setupGameToggle();
|
this.setupGameToggle();
|
||||||
|
|
||||||
console.log('RoadWorld initialized');
|
// Initialize enhanced features (v4.0)
|
||||||
|
this.initEnhancedFeatures();
|
||||||
|
|
||||||
|
console.log('RoadWorld v4.0 initialized with enhanced features');
|
||||||
|
}
|
||||||
|
|
||||||
|
initEnhancedFeatures() {
|
||||||
|
// Sound Manager (initialize first for audio feedback)
|
||||||
|
this.soundManager = new SoundManager();
|
||||||
|
|
||||||
|
// Achievements System
|
||||||
|
this.achievementsManager = new AchievementsManager(this.gameEngine, this.storageManager);
|
||||||
|
|
||||||
|
// Daily Missions
|
||||||
|
this.missionsManager = new MissionsManager(this.gameEngine, this.storageManager);
|
||||||
|
|
||||||
|
// Weather Effects
|
||||||
|
this.weatherEffects = new WeatherEffects(this.mapManager);
|
||||||
|
this.weatherEffects.init();
|
||||||
|
|
||||||
|
// Minimap
|
||||||
|
this.minimap = new Minimap(this.mapManager);
|
||||||
|
this.minimap.init();
|
||||||
|
|
||||||
|
// Statistics Panel
|
||||||
|
this.statisticsPanel = new StatisticsPanel(
|
||||||
|
this.gameEngine,
|
||||||
|
this.achievementsManager,
|
||||||
|
this.missionsManager
|
||||||
|
);
|
||||||
|
this.statisticsPanel.init();
|
||||||
|
|
||||||
|
// Setup enhanced feature controls
|
||||||
|
this.setupEnhancedControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEnhancedControls() {
|
||||||
|
// Stats button
|
||||||
|
const statsBtn = document.getElementById('btn-stats');
|
||||||
|
if (statsBtn) {
|
||||||
|
statsBtn.addEventListener('click', () => {
|
||||||
|
this.statisticsPanel.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather toggle
|
||||||
|
const weatherBtn = document.getElementById('btn-weather');
|
||||||
|
if (weatherBtn) {
|
||||||
|
weatherBtn.addEventListener('click', () => {
|
||||||
|
const isActive = this.weatherEffects.toggle();
|
||||||
|
weatherBtn.classList.toggle('active', isActive);
|
||||||
|
this.showNotification(isActive ? 'Weather effects enabled' : 'Weather effects disabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sound toggle
|
||||||
|
const soundBtn = document.getElementById('btn-sound');
|
||||||
|
if (soundBtn) {
|
||||||
|
soundBtn.addEventListener('click', () => {
|
||||||
|
const isEnabled = this.soundManager.toggle();
|
||||||
|
soundBtn.classList.toggle('active', isEnabled);
|
||||||
|
soundBtn.textContent = isEnabled ? '🔊' : '🔇';
|
||||||
|
this.showNotification(isEnabled ? 'Sound enabled' : 'Sound muted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimap toggle
|
||||||
|
const minimapBtn = document.getElementById('btn-minimap');
|
||||||
|
if (minimapBtn) {
|
||||||
|
minimapBtn.addEventListener('click', () => {
|
||||||
|
this.minimap.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupGameToggle() {
|
setupGameToggle() {
|
||||||
@@ -102,6 +190,11 @@ class RoadWorldApp {
|
|||||||
activateGameMode() {
|
activateGameMode() {
|
||||||
console.log('🎮 Game Mode Activated!');
|
console.log('🎮 Game Mode Activated!');
|
||||||
|
|
||||||
|
// Initialize sound on user interaction
|
||||||
|
if (this.soundManager) {
|
||||||
|
this.soundManager.init();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize game
|
// Initialize game
|
||||||
this.gameEngine.init();
|
this.gameEngine.init();
|
||||||
|
|
||||||
@@ -117,12 +210,22 @@ class RoadWorldApp {
|
|||||||
// Setup map movement to generate collectibles
|
// Setup map movement to generate collectibles
|
||||||
this.mapManager.map.on('moveend', this.onMapMoveGame.bind(this));
|
this.mapManager.map.on('moveend', this.onMapMoveGame.bind(this));
|
||||||
|
|
||||||
|
// Setup collectible collection handler
|
||||||
|
this.collectiblesRenderer.onCollect = (collectible) => {
|
||||||
|
this.onCollectItem(collectible);
|
||||||
|
};
|
||||||
|
|
||||||
// Initial HUD update
|
// Initial HUD update
|
||||||
this.updateGameHUD();
|
this.updateGameHUD();
|
||||||
|
|
||||||
// Show collectibles
|
// Show collectibles
|
||||||
this.collectiblesRenderer.renderAll();
|
this.collectiblesRenderer.renderAll();
|
||||||
|
|
||||||
|
// Play ambient sound
|
||||||
|
if (this.soundManager) {
|
||||||
|
this.soundManager.playAmbient('explore');
|
||||||
|
}
|
||||||
|
|
||||||
this.showNotification('🎮 Game Mode Activated! Click to move your avatar.');
|
this.showNotification('🎮 Game Mode Activated! Click to move your avatar.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,16 +252,31 @@ class RoadWorldApp {
|
|||||||
|
|
||||||
const lngLat = [e.lngLat.lng, e.lngLat.lat];
|
const lngLat = [e.lngLat.lng, e.lngLat.lat];
|
||||||
|
|
||||||
|
// Play move sound
|
||||||
|
if (this.soundManager) {
|
||||||
|
this.soundManager.playMove();
|
||||||
|
}
|
||||||
|
|
||||||
// Move player
|
// Move player
|
||||||
const distance = this.gameEngine.movePlayer(lngLat);
|
const distance = this.gameEngine.movePlayer(lngLat);
|
||||||
|
|
||||||
// Update avatar position
|
// Update avatar position
|
||||||
this.playerAvatar.updatePosition(lngLat);
|
this.playerAvatar.updatePosition(lngLat);
|
||||||
|
|
||||||
|
// Update minimap player position
|
||||||
|
if (this.minimap) {
|
||||||
|
this.minimap.updatePlayerPosition(lngLat);
|
||||||
|
}
|
||||||
|
|
||||||
// Award XP for movement (1 XP per 10 meters)
|
// Award XP for movement (1 XP per 10 meters)
|
||||||
if (distance > 10) {
|
if (distance > 10) {
|
||||||
const xp = Math.floor(distance / 10);
|
const xp = Math.floor(distance / 10);
|
||||||
this.gameEngine.addXP(xp, 'movement');
|
this.gameEngine.addXP(xp, 'movement');
|
||||||
|
|
||||||
|
// Update missions
|
||||||
|
if (this.missionsManager) {
|
||||||
|
this.missionsManager.updateProgress('xp', xp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for level up
|
// Check for level up
|
||||||
@@ -167,6 +285,9 @@ class RoadWorldApp {
|
|||||||
this.onLevelUp(levelUp);
|
this.onLevelUp(levelUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check achievements
|
||||||
|
this.checkAchievements();
|
||||||
|
|
||||||
// Update HUD
|
// Update HUD
|
||||||
this.updateGameHUD();
|
this.updateGameHUD();
|
||||||
|
|
||||||
@@ -174,6 +295,86 @@ class RoadWorldApp {
|
|||||||
this.collectiblesRenderer.refreshVisibleCollectibles();
|
this.collectiblesRenderer.refreshVisibleCollectibles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCollectItem(collectible) {
|
||||||
|
// Play collection sound
|
||||||
|
if (this.soundManager) {
|
||||||
|
this.soundManager.playCollect(collectible.rarity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update missions
|
||||||
|
if (this.missionsManager) {
|
||||||
|
this.missionsManager.updateProgress('item', 1);
|
||||||
|
this.missionsManager.updateProgress(collectible.type, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show collection notification
|
||||||
|
this.showCollectionNotification(collectible);
|
||||||
|
|
||||||
|
// Check achievements
|
||||||
|
this.checkAchievements();
|
||||||
|
|
||||||
|
// Update HUD
|
||||||
|
this.updateGameHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
showCollectionNotification(collectible) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `collection-notification ${collectible.rarity}`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="collection-icon">${collectible.icon}</div>
|
||||||
|
<div class="collection-info">
|
||||||
|
<div class="collection-name">${collectible.type.charAt(0).toUpperCase() + collectible.type.slice(1)}</div>
|
||||||
|
<div class="collection-xp">+${collectible.xp} XP</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
setTimeout(() => notification.classList.add('show'), 10);
|
||||||
|
|
||||||
|
// Remove after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAchievements() {
|
||||||
|
if (!this.achievementsManager || !this.gameEngine.player) return;
|
||||||
|
|
||||||
|
const newAchievements = this.achievementsManager.checkAchievements(this.gameEngine.player);
|
||||||
|
|
||||||
|
newAchievements.forEach(achievement => {
|
||||||
|
this.showAchievementNotification(achievement);
|
||||||
|
if (this.soundManager) {
|
||||||
|
this.soundManager.playAchievement();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showAchievementNotification(achievement) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `achievement-notification ${achievement.rarity}`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="achievement-unlock-icon">${achievement.icon}</div>
|
||||||
|
<div class="achievement-unlock-info">
|
||||||
|
<div class="achievement-unlock-title">Achievement Unlocked!</div>
|
||||||
|
<div class="achievement-unlock-name">${achievement.name}</div>
|
||||||
|
<div class="achievement-unlock-xp">+${achievement.xpReward} XP</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
setTimeout(() => notification.classList.add('show'), 10);
|
||||||
|
|
||||||
|
// Remove after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
onMapMoveGame() {
|
onMapMoveGame() {
|
||||||
if (!this.gameActive) return;
|
if (!this.gameActive) return;
|
||||||
|
|
||||||
@@ -188,9 +389,17 @@ class RoadWorldApp {
|
|||||||
onLevelUp(levelInfo) {
|
onLevelUp(levelInfo) {
|
||||||
console.log(`🎉 LEVEL UP! Now level ${levelInfo.level}`);
|
console.log(`🎉 LEVEL UP! Now level ${levelInfo.level}`);
|
||||||
|
|
||||||
|
// Play level up sound
|
||||||
|
if (this.soundManager) {
|
||||||
|
this.soundManager.playLevelUp();
|
||||||
|
}
|
||||||
|
|
||||||
// Update avatar
|
// Update avatar
|
||||||
this.playerAvatar.updateLevel();
|
this.playerAvatar.updateLevel();
|
||||||
|
|
||||||
|
// Check achievements on level up
|
||||||
|
this.checkAchievements();
|
||||||
|
|
||||||
// Show special notification
|
// Show special notification
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = 'notification level-up-notification';
|
notification.className = 'notification level-up-notification';
|
||||||
@@ -235,6 +444,17 @@ class RoadWorldApp {
|
|||||||
`${(stats.distanceTraveled / 1000).toFixed(2)} km`;
|
`${(stats.distanceTraveled / 1000).toFixed(2)} km`;
|
||||||
document.getElementById('hud-distance').textContent = distanceKm;
|
document.getElementById('hud-distance').textContent = distanceKm;
|
||||||
document.getElementById('hud-collected').textContent = stats.itemsCollected;
|
document.getElementById('hud-collected').textContent = stats.itemsCollected;
|
||||||
|
|
||||||
|
// Missions and Achievements
|
||||||
|
if (this.missionsManager) {
|
||||||
|
const completedMissions = this.missionsManager.getCompletedCount();
|
||||||
|
document.getElementById('hud-missions').textContent = completedMissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.achievementsManager) {
|
||||||
|
const unlockedCount = this.achievementsManager.unlockedAchievements.size;
|
||||||
|
document.getElementById('hud-achievements').textContent = unlockedCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupMapEvents() {
|
setupMapEvents() {
|
||||||
|
|||||||
247
src/js/minimap.js
Normal file
247
src/js/minimap.js
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
// Mini-map Controller for RoadWorld
|
||||||
|
// Provides a navigation overview in the corner of the screen
|
||||||
|
|
||||||
|
export class Minimap {
|
||||||
|
constructor(mapManager) {
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.minimapElement = null;
|
||||||
|
this.minimapMap = null;
|
||||||
|
this.playerMarker = null;
|
||||||
|
this.isVisible = true;
|
||||||
|
this.isExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createMinimapContainer();
|
||||||
|
this.initMinimapMap();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.startSync();
|
||||||
|
|
||||||
|
console.log('🗺️ Minimap initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createMinimapContainer() {
|
||||||
|
this.minimapElement = document.createElement('div');
|
||||||
|
this.minimapElement.className = 'minimap-container';
|
||||||
|
this.minimapElement.innerHTML = `
|
||||||
|
<div class="minimap-header">
|
||||||
|
<span class="minimap-title">MINIMAP</span>
|
||||||
|
<div class="minimap-controls">
|
||||||
|
<button class="minimap-btn" id="minimap-expand" title="Expand">⤢</button>
|
||||||
|
<button class="minimap-btn" id="minimap-toggle" title="Toggle">−</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="minimap-wrapper">
|
||||||
|
<div id="minimap" class="minimap"></div>
|
||||||
|
<div class="minimap-compass">
|
||||||
|
<div class="compass-needle" id="compass-needle"></div>
|
||||||
|
<span class="compass-label north">N</span>
|
||||||
|
<span class="compass-label east">E</span>
|
||||||
|
<span class="compass-label south">S</span>
|
||||||
|
<span class="compass-label west">W</span>
|
||||||
|
</div>
|
||||||
|
<div class="minimap-player-indicator"></div>
|
||||||
|
</div>
|
||||||
|
<div class="minimap-coords" id="minimap-coords">0°, 0°</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this.minimapElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
initMinimapMap() {
|
||||||
|
// Create a simplified map for the minimap
|
||||||
|
const mainCenter = this.mapManager.getCenter();
|
||||||
|
const mainZoom = this.mapManager.getZoom();
|
||||||
|
|
||||||
|
this.minimapMap = new maplibregl.Map({
|
||||||
|
container: 'minimap',
|
||||||
|
style: {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
'carto-dark': {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'carto-dark',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'carto-dark',
|
||||||
|
minzoom: 0,
|
||||||
|
maxzoom: 18
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
center: [mainCenter.lng, mainCenter.lat],
|
||||||
|
zoom: Math.max(0, mainZoom - 4),
|
||||||
|
interactive: false,
|
||||||
|
attributionControl: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add player marker
|
||||||
|
this.createPlayerMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlayerMarker() {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'minimap-player-marker';
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="player-dot"></div>
|
||||||
|
<div class="player-direction"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mainCenter = this.mapManager.getCenter();
|
||||||
|
this.playerMarker = new maplibregl.Marker({
|
||||||
|
element: el,
|
||||||
|
anchor: 'center'
|
||||||
|
})
|
||||||
|
.setLngLat([mainCenter.lng, mainCenter.lat])
|
||||||
|
.addTo(this.minimapMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Toggle button
|
||||||
|
document.getElementById('minimap-toggle').addEventListener('click', () => {
|
||||||
|
this.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand button
|
||||||
|
document.getElementById('minimap-expand').addEventListener('click', () => {
|
||||||
|
this.toggleExpand();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on minimap to navigate
|
||||||
|
this.minimapMap.on('click', (e) => {
|
||||||
|
const { lng, lat } = e.lngLat;
|
||||||
|
this.mapManager.flyTo({
|
||||||
|
center: [lng, lat],
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startSync() {
|
||||||
|
// Sync minimap with main map
|
||||||
|
this.mapManager.on('move', () => this.syncPosition());
|
||||||
|
this.mapManager.on('zoom', () => this.syncPosition());
|
||||||
|
this.mapManager.on('rotate', () => this.syncRotation());
|
||||||
|
}
|
||||||
|
|
||||||
|
syncPosition() {
|
||||||
|
if (!this.minimapMap || !this.isVisible) return;
|
||||||
|
|
||||||
|
const center = this.mapManager.getCenter();
|
||||||
|
const zoom = this.mapManager.getZoom();
|
||||||
|
|
||||||
|
this.minimapMap.setCenter([center.lng, center.lat]);
|
||||||
|
this.minimapMap.setZoom(Math.max(0, zoom - 4));
|
||||||
|
|
||||||
|
// Update player marker
|
||||||
|
if (this.playerMarker) {
|
||||||
|
this.playerMarker.setLngLat([center.lng, center.lat]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update coordinates display
|
||||||
|
const coordsEl = document.getElementById('minimap-coords');
|
||||||
|
if (coordsEl) {
|
||||||
|
coordsEl.textContent = `${center.lat.toFixed(4)}°, ${center.lng.toFixed(4)}°`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncRotation() {
|
||||||
|
const bearing = this.mapManager.getBearing();
|
||||||
|
|
||||||
|
// Update compass needle
|
||||||
|
const needle = document.getElementById('compass-needle');
|
||||||
|
if (needle) {
|
||||||
|
needle.style.transform = `rotate(${-bearing}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player direction indicator
|
||||||
|
const directionEl = document.querySelector('.player-direction');
|
||||||
|
if (directionEl) {
|
||||||
|
directionEl.style.transform = `rotate(${bearing}deg)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isVisible = !this.isVisible;
|
||||||
|
|
||||||
|
const wrapper = this.minimapElement.querySelector('.minimap-wrapper');
|
||||||
|
const coords = this.minimapElement.querySelector('.minimap-coords');
|
||||||
|
const toggleBtn = document.getElementById('minimap-toggle');
|
||||||
|
|
||||||
|
if (this.isVisible) {
|
||||||
|
wrapper.style.display = 'block';
|
||||||
|
coords.style.display = 'block';
|
||||||
|
toggleBtn.textContent = '−';
|
||||||
|
this.syncPosition();
|
||||||
|
} else {
|
||||||
|
wrapper.style.display = 'none';
|
||||||
|
coords.style.display = 'none';
|
||||||
|
toggleBtn.textContent = '+';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleExpand() {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
this.minimapElement.classList.toggle('expanded', this.isExpanded);
|
||||||
|
|
||||||
|
const expandBtn = document.getElementById('minimap-expand');
|
||||||
|
expandBtn.textContent = this.isExpanded ? '⤡' : '⤢';
|
||||||
|
|
||||||
|
// Resize the minimap
|
||||||
|
setTimeout(() => {
|
||||||
|
this.minimapMap.resize();
|
||||||
|
this.syncPosition();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayerPosition(position) {
|
||||||
|
if (this.playerMarker && position) {
|
||||||
|
this.playerMarker.setLngLat(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyle(styleUrl) {
|
||||||
|
if (this.minimapMap) {
|
||||||
|
// Keep dark theme for minimap for visibility
|
||||||
|
this.minimapMap.setStyle({
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
'carto-dark': {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'carto-dark',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'carto-dark',
|
||||||
|
minzoom: 0,
|
||||||
|
maxzoom: 18
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.minimapElement.style.display = 'block';
|
||||||
|
this.isVisible = true;
|
||||||
|
this.syncPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.minimapElement.style.display = 'none';
|
||||||
|
this.isVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.minimapMap) {
|
||||||
|
this.minimapMap.remove();
|
||||||
|
}
|
||||||
|
if (this.minimapElement) {
|
||||||
|
this.minimapElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
283
src/js/missionsManager.js
Normal file
283
src/js/missionsManager.js
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// Daily Missions System for RoadWorld
|
||||||
|
// Generates daily challenges with XP rewards
|
||||||
|
|
||||||
|
export class MissionsManager {
|
||||||
|
constructor(gameEngine, storageManager) {
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
// Mission templates
|
||||||
|
this.missionTemplates = this.defineMissionTemplates();
|
||||||
|
|
||||||
|
// Load current missions
|
||||||
|
this.activeMissions = this.loadMissions();
|
||||||
|
|
||||||
|
// Check if we need new daily missions
|
||||||
|
this.checkDailyReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineMissionTemplates() {
|
||||||
|
return {
|
||||||
|
// Distance missions
|
||||||
|
walk_distance: {
|
||||||
|
type: 'distance',
|
||||||
|
name: 'Take a Walk',
|
||||||
|
description: 'Travel {target} meters',
|
||||||
|
icon: '🚶',
|
||||||
|
targets: [100, 250, 500, 1000, 2000],
|
||||||
|
xpRewards: [20, 40, 80, 150, 300],
|
||||||
|
getProgress: (player) => player.stats.distanceTraveled
|
||||||
|
},
|
||||||
|
run_distance: {
|
||||||
|
type: 'distance',
|
||||||
|
name: 'Go the Distance',
|
||||||
|
description: 'Travel {target} kilometers',
|
||||||
|
icon: '🏃',
|
||||||
|
targets: [1, 2, 5, 10, 25],
|
||||||
|
xpRewards: [50, 100, 200, 400, 1000],
|
||||||
|
getProgress: (player) => player.stats.distanceTraveled / 1000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Collection missions
|
||||||
|
collect_stars: {
|
||||||
|
type: 'collect',
|
||||||
|
name: 'Star Gazer',
|
||||||
|
description: 'Collect {target} stars today',
|
||||||
|
icon: '⭐',
|
||||||
|
targets: [3, 5, 10, 20, 50],
|
||||||
|
xpRewards: [30, 50, 100, 200, 500],
|
||||||
|
trackKey: 'dailyStars'
|
||||||
|
},
|
||||||
|
collect_gems: {
|
||||||
|
type: 'collect',
|
||||||
|
name: 'Gem Hunter',
|
||||||
|
description: 'Collect {target} gems today',
|
||||||
|
icon: '💎',
|
||||||
|
targets: [1, 2, 5, 10, 20],
|
||||||
|
xpRewards: [50, 100, 250, 500, 1000],
|
||||||
|
trackKey: 'dailyGems'
|
||||||
|
},
|
||||||
|
collect_any: {
|
||||||
|
type: 'collect',
|
||||||
|
name: 'Treasure Seeker',
|
||||||
|
description: 'Collect {target} items today',
|
||||||
|
icon: '✨',
|
||||||
|
targets: [5, 10, 20, 50, 100],
|
||||||
|
xpRewards: [25, 50, 100, 250, 500],
|
||||||
|
trackKey: 'dailyItems'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Rare collection missions
|
||||||
|
collect_trophy: {
|
||||||
|
type: 'collect',
|
||||||
|
name: 'Trophy Hunter',
|
||||||
|
description: 'Collect a trophy today',
|
||||||
|
icon: '🏆',
|
||||||
|
targets: [1],
|
||||||
|
xpRewards: [200],
|
||||||
|
trackKey: 'dailyTrophies'
|
||||||
|
},
|
||||||
|
collect_key: {
|
||||||
|
type: 'collect',
|
||||||
|
name: 'Key Quest',
|
||||||
|
description: 'Find a legendary key',
|
||||||
|
icon: '🗝️',
|
||||||
|
targets: [1],
|
||||||
|
xpRewards: [500],
|
||||||
|
trackKey: 'dailyKeys'
|
||||||
|
},
|
||||||
|
|
||||||
|
// XP missions
|
||||||
|
earn_xp: {
|
||||||
|
type: 'xp',
|
||||||
|
name: 'XP Grinder',
|
||||||
|
description: 'Earn {target} XP today',
|
||||||
|
icon: '📈',
|
||||||
|
targets: [50, 100, 250, 500, 1000],
|
||||||
|
xpRewards: [25, 50, 125, 250, 500],
|
||||||
|
trackKey: 'dailyXP'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Exploration missions
|
||||||
|
explore_locations: {
|
||||||
|
type: 'explore',
|
||||||
|
name: 'Explorer',
|
||||||
|
description: 'Visit {target} different locations',
|
||||||
|
icon: '🗺️',
|
||||||
|
targets: [3, 5, 10, 20],
|
||||||
|
xpRewards: [50, 100, 200, 500],
|
||||||
|
trackKey: 'dailyLocations'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMissions() {
|
||||||
|
return this.storageManager.data.dailyMissions || {
|
||||||
|
missions: [],
|
||||||
|
lastReset: null,
|
||||||
|
dailyProgress: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMissions() {
|
||||||
|
this.storageManager.data.dailyMissions = this.activeMissions;
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDailyReset() {
|
||||||
|
const now = new Date();
|
||||||
|
const today = now.toDateString();
|
||||||
|
|
||||||
|
if (this.activeMissions.lastReset !== today) {
|
||||||
|
this.generateDailyMissions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDailyMissions() {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Reset daily progress
|
||||||
|
this.activeMissions.dailyProgress = {
|
||||||
|
dailyStars: 0,
|
||||||
|
dailyGems: 0,
|
||||||
|
dailyTrophies: 0,
|
||||||
|
dailyKeys: 0,
|
||||||
|
dailyItems: 0,
|
||||||
|
dailyXP: 0,
|
||||||
|
dailyLocations: 0,
|
||||||
|
startDistance: this.gameEngine?.player?.stats?.distanceTraveled || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate 3 random missions of varying difficulty
|
||||||
|
const templates = Object.entries(this.missionTemplates);
|
||||||
|
const shuffled = templates.sort(() => Math.random() - 0.5);
|
||||||
|
const selected = shuffled.slice(0, 3);
|
||||||
|
|
||||||
|
this.activeMissions.missions = selected.map(([key, template], index) => {
|
||||||
|
// Vary difficulty: easy, medium, hard
|
||||||
|
const difficultyIndex = Math.min(index, template.targets.length - 1);
|
||||||
|
const target = template.targets[difficultyIndex];
|
||||||
|
const xpReward = template.xpRewards[difficultyIndex];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `daily_${key}_${Date.now()}`,
|
||||||
|
templateKey: key,
|
||||||
|
name: template.name,
|
||||||
|
description: template.description.replace('{target}', target),
|
||||||
|
icon: template.icon,
|
||||||
|
type: template.type,
|
||||||
|
target: target,
|
||||||
|
xpReward: xpReward,
|
||||||
|
progress: 0,
|
||||||
|
completed: false,
|
||||||
|
claimed: false,
|
||||||
|
trackKey: template.trackKey
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeMissions.lastReset = now.toDateString();
|
||||||
|
this.saveMissions();
|
||||||
|
|
||||||
|
console.log('🎯 New daily missions generated!');
|
||||||
|
return this.activeMissions.missions;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(type, amount = 1) {
|
||||||
|
// Update daily progress tracking
|
||||||
|
const progressKeys = {
|
||||||
|
star: 'dailyStars',
|
||||||
|
gem: 'dailyGems',
|
||||||
|
trophy: 'dailyTrophies',
|
||||||
|
key: 'dailyKeys',
|
||||||
|
item: 'dailyItems',
|
||||||
|
xp: 'dailyXP',
|
||||||
|
location: 'dailyLocations'
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = progressKeys[type];
|
||||||
|
if (key && this.activeMissions.dailyProgress) {
|
||||||
|
this.activeMissions.dailyProgress[key] =
|
||||||
|
(this.activeMissions.dailyProgress[key] || 0) + amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mission completion
|
||||||
|
this.checkMissionProgress();
|
||||||
|
this.saveMissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMissionProgress() {
|
||||||
|
if (!this.activeMissions.missions) return;
|
||||||
|
|
||||||
|
this.activeMissions.missions.forEach(mission => {
|
||||||
|
if (mission.completed) return;
|
||||||
|
|
||||||
|
let currentProgress = 0;
|
||||||
|
|
||||||
|
switch (mission.type) {
|
||||||
|
case 'distance':
|
||||||
|
const startDist = this.activeMissions.dailyProgress.startDistance || 0;
|
||||||
|
const currentDist = this.gameEngine?.player?.stats?.distanceTraveled || 0;
|
||||||
|
currentProgress = currentDist - startDist;
|
||||||
|
if (mission.target >= 1 && mission.target <= 100) {
|
||||||
|
// Target is in km, convert progress
|
||||||
|
currentProgress = currentProgress / 1000;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'collect':
|
||||||
|
case 'xp':
|
||||||
|
case 'explore':
|
||||||
|
currentProgress = this.activeMissions.dailyProgress[mission.trackKey] || 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mission.progress = Math.min(currentProgress, mission.target);
|
||||||
|
|
||||||
|
if (mission.progress >= mission.target && !mission.completed) {
|
||||||
|
mission.completed = true;
|
||||||
|
console.log(`🎯 Mission Complete: ${mission.name}!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
claimMissionReward(missionId) {
|
||||||
|
const mission = this.activeMissions.missions.find(m => m.id === missionId);
|
||||||
|
if (!mission || !mission.completed || mission.claimed) return null;
|
||||||
|
|
||||||
|
mission.claimed = true;
|
||||||
|
this.saveMissions();
|
||||||
|
|
||||||
|
// Award XP
|
||||||
|
if (this.gameEngine) {
|
||||||
|
this.gameEngine.addXP(mission.xpReward, `mission: ${mission.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`💰 Claimed ${mission.xpReward} XP from ${mission.name}!`);
|
||||||
|
return mission;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMissions() {
|
||||||
|
this.checkMissionProgress();
|
||||||
|
return this.activeMissions.missions || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompletedCount() {
|
||||||
|
return (this.activeMissions.missions || []).filter(m => m.completed).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClaimableCount() {
|
||||||
|
return (this.activeMissions.missions || []).filter(m => m.completed && !m.claimed).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeUntilReset() {
|
||||||
|
const now = new Date();
|
||||||
|
const tomorrow = new Date(now);
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
tomorrow.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const diff = tomorrow - now;
|
||||||
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
return { hours, minutes, formatted: `${hours}h ${minutes}m` };
|
||||||
|
}
|
||||||
|
}
|
||||||
238
src/js/soundManager.js
Normal file
238
src/js/soundManager.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// Sound Manager for RoadWorld
|
||||||
|
// Provides audio feedback for game events using Web Audio API
|
||||||
|
|
||||||
|
export class SoundManager {
|
||||||
|
constructor() {
|
||||||
|
this.audioContext = null;
|
||||||
|
this.isEnabled = true;
|
||||||
|
this.volume = 0.5;
|
||||||
|
this.sounds = {};
|
||||||
|
|
||||||
|
// Try to initialize on user interaction
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
try {
|
||||||
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
this.masterGain = this.audioContext.createGain();
|
||||||
|
this.masterGain.connect(this.audioContext.destination);
|
||||||
|
this.masterGain.gain.value = this.volume;
|
||||||
|
this.initialized = true;
|
||||||
|
console.log('🔊 Sound Manager initialized');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Web Audio API not supported:', e);
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureContext() {
|
||||||
|
if (!this.initialized) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||||
|
this.audioContext.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate sounds programmatically (no external files needed)
|
||||||
|
generateTone(frequency, duration, type = 'sine', attack = 0.01, decay = 0.1) {
|
||||||
|
this.ensureContext();
|
||||||
|
if (!this.audioContext) return;
|
||||||
|
|
||||||
|
const oscillator = this.audioContext.createOscillator();
|
||||||
|
const gainNode = this.audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(this.masterGain);
|
||||||
|
|
||||||
|
oscillator.frequency.value = frequency;
|
||||||
|
oscillator.type = type;
|
||||||
|
|
||||||
|
const now = this.audioContext.currentTime;
|
||||||
|
gainNode.gain.setValueAtTime(0, now);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0.3, now + attack);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration);
|
||||||
|
|
||||||
|
oscillator.start(now);
|
||||||
|
oscillator.stop(now + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sound effects for different game events
|
||||||
|
playCollect(rarity = 'common') {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
// Different sounds for different rarities
|
||||||
|
switch (rarity) {
|
||||||
|
case 'common':
|
||||||
|
this.generateTone(880, 0.15, 'sine');
|
||||||
|
setTimeout(() => this.generateTone(1100, 0.1, 'sine'), 50);
|
||||||
|
break;
|
||||||
|
case 'rare':
|
||||||
|
this.generateTone(660, 0.1, 'sine');
|
||||||
|
setTimeout(() => this.generateTone(880, 0.1, 'sine'), 50);
|
||||||
|
setTimeout(() => this.generateTone(1100, 0.15, 'sine'), 100);
|
||||||
|
break;
|
||||||
|
case 'epic':
|
||||||
|
this.generateTone(440, 0.1, 'sine');
|
||||||
|
setTimeout(() => this.generateTone(660, 0.1, 'sine'), 80);
|
||||||
|
setTimeout(() => this.generateTone(880, 0.1, 'sine'), 160);
|
||||||
|
setTimeout(() => this.generateTone(1100, 0.2, 'sine'), 240);
|
||||||
|
break;
|
||||||
|
case 'legendary':
|
||||||
|
this.playFanfare();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playLevelUp() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
// Triumphant ascending notes
|
||||||
|
const notes = [523, 659, 784, 1047]; // C5, E5, G5, C6
|
||||||
|
notes.forEach((freq, i) => {
|
||||||
|
setTimeout(() => this.generateTone(freq, 0.2, 'sine'), i * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a sustain chord
|
||||||
|
setTimeout(() => {
|
||||||
|
this.generateTone(523, 0.5, 'sine');
|
||||||
|
this.generateTone(659, 0.5, 'sine');
|
||||||
|
this.generateTone(784, 0.5, 'sine');
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
playAchievement() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
// Achievement unlock sound
|
||||||
|
const notes = [392, 494, 587, 784]; // G4, B4, D5, G5
|
||||||
|
notes.forEach((freq, i) => {
|
||||||
|
setTimeout(() => this.generateTone(freq, 0.15, 'sine'), i * 80);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.generateTone(784, 0.3, 'triangle');
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
|
||||||
|
playMissionComplete() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
// Mission complete jingle
|
||||||
|
const notes = [440, 554, 659, 880]; // A4, C#5, E5, A5
|
||||||
|
notes.forEach((freq, i) => {
|
||||||
|
setTimeout(() => this.generateTone(freq, 0.12, 'sine'), i * 70);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
playClick() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
this.generateTone(800, 0.05, 'sine');
|
||||||
|
}
|
||||||
|
|
||||||
|
playMove() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
// Subtle movement sound
|
||||||
|
this.generateTone(300, 0.08, 'sine');
|
||||||
|
setTimeout(() => this.generateTone(350, 0.06, 'sine'), 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
playError() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
this.generateTone(200, 0.15, 'sawtooth');
|
||||||
|
setTimeout(() => this.generateTone(150, 0.2, 'sawtooth'), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
playNotification() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
this.generateTone(880, 0.1, 'sine');
|
||||||
|
setTimeout(() => this.generateTone(660, 0.15, 'sine'), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
playFanfare() {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
// Epic fanfare for legendary items
|
||||||
|
const melody = [
|
||||||
|
{ freq: 523, delay: 0, duration: 0.15 },
|
||||||
|
{ freq: 659, delay: 100, duration: 0.15 },
|
||||||
|
{ freq: 784, delay: 200, duration: 0.15 },
|
||||||
|
{ freq: 1047, delay: 300, duration: 0.3 },
|
||||||
|
{ freq: 880, delay: 500, duration: 0.15 },
|
||||||
|
{ freq: 1047, delay: 600, duration: 0.4 }
|
||||||
|
];
|
||||||
|
|
||||||
|
melody.forEach(note => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.generateTone(note.freq, note.duration, 'sine');
|
||||||
|
}, note.delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
playAmbient(type = 'explore') {
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
this.ensureContext();
|
||||||
|
|
||||||
|
// Ambient pad sounds
|
||||||
|
const chords = {
|
||||||
|
explore: [261, 329, 392], // C major
|
||||||
|
night: [220, 261, 329], // A minor
|
||||||
|
dawn: [293, 369, 440] // D major
|
||||||
|
};
|
||||||
|
|
||||||
|
const chord = chords[type] || chords.explore;
|
||||||
|
|
||||||
|
chord.forEach(freq => {
|
||||||
|
const oscillator = this.audioContext.createOscillator();
|
||||||
|
const gainNode = this.audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(this.masterGain);
|
||||||
|
|
||||||
|
oscillator.frequency.value = freq;
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
|
||||||
|
const now = this.audioContext.currentTime;
|
||||||
|
gainNode.gain.setValueAtTime(0, now);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0.05, now + 1);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0, now + 4);
|
||||||
|
|
||||||
|
oscillator.start(now);
|
||||||
|
oscillator.stop(now + 4);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(value) {
|
||||||
|
this.volume = Math.max(0, Math.min(1, value));
|
||||||
|
if (this.masterGain) {
|
||||||
|
this.masterGain.gain.value = this.volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isEnabled = !this.isEnabled;
|
||||||
|
return this.isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
mute() {
|
||||||
|
this.isEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmute() {
|
||||||
|
this.isEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
341
src/js/statisticsPanel.js
Normal file
341
src/js/statisticsPanel.js
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
// Statistics Panel for RoadWorld
|
||||||
|
// Displays comprehensive player statistics and progress
|
||||||
|
|
||||||
|
export class StatisticsPanel {
|
||||||
|
constructor(gameEngine, achievementsManager, missionsManager) {
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.achievementsManager = achievementsManager;
|
||||||
|
this.missionsManager = missionsManager;
|
||||||
|
this.panelElement = null;
|
||||||
|
this.isVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
console.log('📊 Statistics Panel initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'statistics-panel ui-overlay';
|
||||||
|
this.panelElement.id = 'statistics-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>Player Statistics</span>
|
||||||
|
<button class="panel-close" id="stats-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content stats-content">
|
||||||
|
<div class="stats-tabs">
|
||||||
|
<button class="stats-tab active" data-tab="overview">Overview</button>
|
||||||
|
<button class="stats-tab" data-tab="achievements">Achievements</button>
|
||||||
|
<button class="stats-tab" data-tab="missions">Missions</button>
|
||||||
|
<button class="stats-tab" data-tab="inventory">Inventory</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-tab-content" id="tab-overview">
|
||||||
|
<div class="stats-profile" id="stats-profile"></div>
|
||||||
|
<div class="stats-grid" id="stats-grid"></div>
|
||||||
|
<div class="stats-chart" id="stats-chart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-tab-content" id="tab-achievements" style="display: none;">
|
||||||
|
<div class="achievements-progress" id="achievements-progress"></div>
|
||||||
|
<div class="achievements-grid" id="achievements-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-tab-content" id="tab-missions" style="display: none;">
|
||||||
|
<div class="missions-header" id="missions-header"></div>
|
||||||
|
<div class="missions-list" id="missions-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-tab-content" id="tab-inventory" style="display: none;">
|
||||||
|
<div class="inventory-summary" id="inventory-summary"></div>
|
||||||
|
<div class="inventory-details" id="inventory-details"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Close button
|
||||||
|
document.getElementById('stats-close').addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
this.panelElement.querySelectorAll('.stats-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
const tabName = e.target.dataset.tab;
|
||||||
|
this.switchTab(tabName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switchTab(tabName) {
|
||||||
|
// Update active tab button
|
||||||
|
this.panelElement.querySelectorAll('.stats-tab').forEach(tab => {
|
||||||
|
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide tab content
|
||||||
|
this.panelElement.querySelectorAll('.stats-tab-content').forEach(content => {
|
||||||
|
content.style.display = content.id === `tab-${tabName}` ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh content
|
||||||
|
this.refreshTab(tabName);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTab(tabName) {
|
||||||
|
switch (tabName) {
|
||||||
|
case 'overview':
|
||||||
|
this.renderOverview();
|
||||||
|
break;
|
||||||
|
case 'achievements':
|
||||||
|
this.renderAchievements();
|
||||||
|
break;
|
||||||
|
case 'missions':
|
||||||
|
this.renderMissions();
|
||||||
|
break;
|
||||||
|
case 'inventory':
|
||||||
|
this.renderInventory();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderOverview() {
|
||||||
|
const player = this.gameEngine.player;
|
||||||
|
const stats = this.gameEngine.getPlayerStats();
|
||||||
|
|
||||||
|
// Profile section
|
||||||
|
document.getElementById('stats-profile').innerHTML = `
|
||||||
|
<div class="profile-avatar" style="background: ${player.avatar.color}">
|
||||||
|
<span class="profile-icon">🧑🚀</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-info">
|
||||||
|
<div class="profile-name">${player.username}</div>
|
||||||
|
<div class="profile-level">Level ${player.level}</div>
|
||||||
|
<div class="profile-xp-bar">
|
||||||
|
<div class="profile-xp-fill" style="width: ${stats.xpProgress}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="profile-xp-text">${player.xp} / ${player.xpToNextLevel} XP</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Stats grid
|
||||||
|
const playTimeHours = Math.floor(player.stats.playTime / 3600000);
|
||||||
|
const playTimeMinutes = Math.floor((player.stats.playTime % 3600000) / 60000);
|
||||||
|
const distanceKm = (player.stats.distanceTraveled / 1000).toFixed(2);
|
||||||
|
|
||||||
|
document.getElementById('stats-grid').innerHTML = `
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card-icon">🚶</div>
|
||||||
|
<div class="stat-card-value">${distanceKm} km</div>
|
||||||
|
<div class="stat-card-label">Distance Traveled</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card-icon">✨</div>
|
||||||
|
<div class="stat-card-value">${player.stats.itemsCollected}</div>
|
||||||
|
<div class="stat-card-label">Items Collected</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card-icon">🏆</div>
|
||||||
|
<div class="stat-card-value">${this.achievementsManager?.unlockedAchievements.size || 0}</div>
|
||||||
|
<div class="stat-card-label">Achievements</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card-icon">⏱️</div>
|
||||||
|
<div class="stat-card-value">${playTimeHours}h ${playTimeMinutes}m</div>
|
||||||
|
<div class="stat-card-label">Play Time</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card-icon">📍</div>
|
||||||
|
<div class="stat-card-value">${player.stats.locationsDiscovered}</div>
|
||||||
|
<div class="stat-card-label">Locations Found</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card-icon">🎯</div>
|
||||||
|
<div class="stat-card-value">${player.stats.missionsCompleted}</div>
|
||||||
|
<div class="stat-card-label">Missions Done</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAchievements() {
|
||||||
|
if (!this.achievementsManager) return;
|
||||||
|
|
||||||
|
const progress = this.achievementsManager.getAchievementProgress();
|
||||||
|
const achievements = this.achievementsManager.getAllAchievements();
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
document.getElementById('achievements-progress').innerHTML = `
|
||||||
|
<div class="achievements-progress-bar">
|
||||||
|
<div class="achievements-progress-fill" style="width: ${progress.percentage}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="achievements-progress-text">
|
||||||
|
${progress.unlocked} / ${progress.total} Achievements (${progress.percentage}%)
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Achievement categories
|
||||||
|
const categories = ['explorer', 'collector', 'level', 'time', 'special'];
|
||||||
|
let gridHTML = '';
|
||||||
|
|
||||||
|
categories.forEach(category => {
|
||||||
|
const categoryAchievements = achievements.filter(a => a.category === category);
|
||||||
|
if (categoryAchievements.length === 0) return;
|
||||||
|
|
||||||
|
gridHTML += `
|
||||||
|
<div class="achievement-category">
|
||||||
|
<h4>${category.charAt(0).toUpperCase() + category.slice(1)}</h4>
|
||||||
|
<div class="achievement-list">
|
||||||
|
${categoryAchievements.map(a => `
|
||||||
|
<div class="achievement-item ${a.unlocked ? 'unlocked' : 'locked'} ${a.rarity}">
|
||||||
|
<div class="achievement-icon">${a.icon}</div>
|
||||||
|
<div class="achievement-info">
|
||||||
|
<div class="achievement-name">${a.name}</div>
|
||||||
|
<div class="achievement-desc">${a.description}</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-reward">+${a.xpReward} XP</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('achievements-grid').innerHTML = gridHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMissions() {
|
||||||
|
if (!this.missionsManager) return;
|
||||||
|
|
||||||
|
const missions = this.missionsManager.getMissions();
|
||||||
|
const timeUntilReset = this.missionsManager.getTimeUntilReset();
|
||||||
|
const claimableCount = this.missionsManager.getClaimableCount();
|
||||||
|
|
||||||
|
// Header with reset timer
|
||||||
|
document.getElementById('missions-header').innerHTML = `
|
||||||
|
<div class="missions-timer">
|
||||||
|
<span class="timer-icon">⏰</span>
|
||||||
|
<span>Resets in ${timeUntilReset.formatted}</span>
|
||||||
|
</div>
|
||||||
|
${claimableCount > 0 ? `<div class="missions-claimable">${claimableCount} reward${claimableCount > 1 ? 's' : ''} available!</div>` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mission list
|
||||||
|
if (missions.length === 0) {
|
||||||
|
document.getElementById('missions-list').innerHTML = `
|
||||||
|
<div class="missions-empty">No active missions. Check back tomorrow!</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('missions-list').innerHTML = missions.map(mission => {
|
||||||
|
const progressPercent = Math.min(100, (mission.progress / mission.target) * 100);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="mission-item ${mission.completed ? 'completed' : ''} ${mission.claimed ? 'claimed' : ''}">
|
||||||
|
<div class="mission-icon">${mission.icon}</div>
|
||||||
|
<div class="mission-info">
|
||||||
|
<div class="mission-name">${mission.name}</div>
|
||||||
|
<div class="mission-desc">${mission.description}</div>
|
||||||
|
<div class="mission-progress-bar">
|
||||||
|
<div class="mission-progress-fill" style="width: ${progressPercent}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mission-progress-text">${mission.progress.toFixed(0)} / ${mission.target}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mission-reward">
|
||||||
|
${mission.claimed ? '✓ Claimed' :
|
||||||
|
mission.completed ? `<button class="claim-btn" data-mission="${mission.id}">Claim ${mission.xpReward} XP</button>` :
|
||||||
|
`+${mission.xpReward} XP`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Add claim button handlers
|
||||||
|
this.panelElement.querySelectorAll('.claim-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const missionId = e.target.dataset.mission;
|
||||||
|
this.missionsManager.claimMissionReward(missionId);
|
||||||
|
this.renderMissions();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInventory() {
|
||||||
|
const inventory = this.gameEngine.getInventorySummary();
|
||||||
|
const items = this.gameEngine.player.inventory.items || [];
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
document.getElementById('inventory-summary').innerHTML = `
|
||||||
|
<div class="inventory-grid">
|
||||||
|
<div class="inventory-item-card">
|
||||||
|
<span class="inv-icon">⭐</span>
|
||||||
|
<span class="inv-count">${inventory.stars}</span>
|
||||||
|
<span class="inv-label">Stars</span>
|
||||||
|
</div>
|
||||||
|
<div class="inventory-item-card">
|
||||||
|
<span class="inv-icon">💎</span>
|
||||||
|
<span class="inv-count">${inventory.gems}</span>
|
||||||
|
<span class="inv-label">Gems</span>
|
||||||
|
</div>
|
||||||
|
<div class="inventory-item-card">
|
||||||
|
<span class="inv-icon">🏆</span>
|
||||||
|
<span class="inv-count">${inventory.trophies}</span>
|
||||||
|
<span class="inv-label">Trophies</span>
|
||||||
|
</div>
|
||||||
|
<div class="inventory-item-card">
|
||||||
|
<span class="inv-icon">🗝️</span>
|
||||||
|
<span class="inv-count">${inventory.keys}</span>
|
||||||
|
<span class="inv-label">Keys</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inventory-total">
|
||||||
|
Total Items: ${inventory.total}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Recent items
|
||||||
|
const recentItems = items.slice(-10).reverse();
|
||||||
|
document.getElementById('inventory-details').innerHTML = `
|
||||||
|
<h4>Recent Finds</h4>
|
||||||
|
<div class="recent-items">
|
||||||
|
${recentItems.length === 0 ? '<div class="no-items">No items collected yet</div>' :
|
||||||
|
recentItems.map(item => `
|
||||||
|
<div class="recent-item ${item.rarity}">
|
||||||
|
<span class="recent-item-icon">${item.icon}</span>
|
||||||
|
<span class="recent-item-type">${item.type}</span>
|
||||||
|
<span class="recent-item-rarity">${item.rarity}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
this.switchTab('overview');
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isVisible = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
336
src/js/weatherEffects.js
Normal file
336
src/js/weatherEffects.js
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
// Weather Effects System for RoadWorld
|
||||||
|
// Adds atmospheric visual effects based on time and simulated weather
|
||||||
|
|
||||||
|
export class WeatherEffects {
|
||||||
|
constructor(mapManager) {
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.weatherContainer = null;
|
||||||
|
this.currentWeather = null;
|
||||||
|
this.timeOfDay = null;
|
||||||
|
this.particles = [];
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
|
// Weather types with their visual properties
|
||||||
|
this.weatherTypes = {
|
||||||
|
clear: {
|
||||||
|
name: 'Clear',
|
||||||
|
icon: '☀️',
|
||||||
|
particles: null,
|
||||||
|
overlay: null,
|
||||||
|
ambientLight: 1.0
|
||||||
|
},
|
||||||
|
cloudy: {
|
||||||
|
name: 'Cloudy',
|
||||||
|
icon: '☁️',
|
||||||
|
particles: null,
|
||||||
|
overlay: 'rgba(100, 100, 120, 0.15)',
|
||||||
|
ambientLight: 0.8
|
||||||
|
},
|
||||||
|
rain: {
|
||||||
|
name: 'Rain',
|
||||||
|
icon: '🌧️',
|
||||||
|
particles: 'rain',
|
||||||
|
overlay: 'rgba(50, 60, 80, 0.25)',
|
||||||
|
ambientLight: 0.6
|
||||||
|
},
|
||||||
|
storm: {
|
||||||
|
name: 'Storm',
|
||||||
|
icon: '⛈️',
|
||||||
|
particles: 'rain-heavy',
|
||||||
|
overlay: 'rgba(30, 40, 60, 0.35)',
|
||||||
|
ambientLight: 0.4,
|
||||||
|
lightning: true
|
||||||
|
},
|
||||||
|
snow: {
|
||||||
|
name: 'Snow',
|
||||||
|
icon: '❄️',
|
||||||
|
particles: 'snow',
|
||||||
|
overlay: 'rgba(200, 210, 230, 0.2)',
|
||||||
|
ambientLight: 0.9
|
||||||
|
},
|
||||||
|
fog: {
|
||||||
|
name: 'Fog',
|
||||||
|
icon: '🌫️',
|
||||||
|
particles: 'fog',
|
||||||
|
overlay: 'rgba(180, 180, 190, 0.4)',
|
||||||
|
ambientLight: 0.7
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createWeatherContainer();
|
||||||
|
this.createOverlay();
|
||||||
|
this.isActive = true;
|
||||||
|
|
||||||
|
// Start with time-based weather
|
||||||
|
this.updateTimeOfDay();
|
||||||
|
this.simulateWeather();
|
||||||
|
|
||||||
|
// Update every minute
|
||||||
|
setInterval(() => this.updateTimeOfDay(), 60000);
|
||||||
|
|
||||||
|
console.log('🌤️ Weather Effects initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createWeatherContainer() {
|
||||||
|
this.weatherContainer = document.createElement('div');
|
||||||
|
this.weatherContainer.className = 'weather-container';
|
||||||
|
this.weatherContainer.innerHTML = `
|
||||||
|
<canvas id="weather-canvas"></canvas>
|
||||||
|
<div class="weather-overlay"></div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this.weatherContainer);
|
||||||
|
|
||||||
|
this.canvas = document.getElementById('weather-canvas');
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.resizeCanvas();
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => this.resizeCanvas());
|
||||||
|
}
|
||||||
|
|
||||||
|
createOverlay() {
|
||||||
|
this.overlay = this.weatherContainer.querySelector('.weather-overlay');
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas() {
|
||||||
|
this.canvas.width = window.innerWidth;
|
||||||
|
this.canvas.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeOfDay() {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
|
||||||
|
let timeOfDay, ambientColor;
|
||||||
|
|
||||||
|
if (hour >= 5 && hour < 7) {
|
||||||
|
timeOfDay = 'dawn';
|
||||||
|
ambientColor = 'rgba(255, 180, 100, 0.1)';
|
||||||
|
} else if (hour >= 7 && hour < 17) {
|
||||||
|
timeOfDay = 'day';
|
||||||
|
ambientColor = 'transparent';
|
||||||
|
} else if (hour >= 17 && hour < 20) {
|
||||||
|
timeOfDay = 'dusk';
|
||||||
|
ambientColor = 'rgba(255, 120, 80, 0.15)';
|
||||||
|
} else {
|
||||||
|
timeOfDay = 'night';
|
||||||
|
ambientColor = 'rgba(20, 30, 60, 0.3)';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeOfDay = timeOfDay;
|
||||||
|
document.body.setAttribute('data-time', timeOfDay);
|
||||||
|
|
||||||
|
// Apply time-based tint
|
||||||
|
if (this.overlay) {
|
||||||
|
this.overlay.style.background = ambientColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeOfDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
simulateWeather() {
|
||||||
|
// Simulate weather based on random chance and time
|
||||||
|
const random = Math.random();
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
const month = new Date().getMonth();
|
||||||
|
|
||||||
|
let weather;
|
||||||
|
|
||||||
|
// Winter months have more snow/fog
|
||||||
|
const isWinter = month === 11 || month === 0 || month === 1;
|
||||||
|
|
||||||
|
if (random < 0.5) {
|
||||||
|
weather = 'clear';
|
||||||
|
} else if (random < 0.65) {
|
||||||
|
weather = 'cloudy';
|
||||||
|
} else if (random < 0.8) {
|
||||||
|
weather = isWinter ? 'snow' : 'rain';
|
||||||
|
} else if (random < 0.9) {
|
||||||
|
weather = 'fog';
|
||||||
|
} else {
|
||||||
|
weather = 'storm';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setWeather(weather);
|
||||||
|
}
|
||||||
|
|
||||||
|
setWeather(weatherType) {
|
||||||
|
const weather = this.weatherTypes[weatherType];
|
||||||
|
if (!weather) return;
|
||||||
|
|
||||||
|
this.currentWeather = weatherType;
|
||||||
|
this.stopParticles();
|
||||||
|
|
||||||
|
// Apply overlay
|
||||||
|
if (weather.overlay) {
|
||||||
|
this.overlay.style.background = weather.overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start particles
|
||||||
|
if (weather.particles) {
|
||||||
|
this.startParticles(weather.particles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lightning effect
|
||||||
|
if (weather.lightning) {
|
||||||
|
this.startLightning();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🌤️ Weather changed to: ${weather.name}`);
|
||||||
|
return weather;
|
||||||
|
}
|
||||||
|
|
||||||
|
startParticles(type) {
|
||||||
|
this.particles = [];
|
||||||
|
|
||||||
|
const particleCount = type === 'rain-heavy' ? 200 :
|
||||||
|
type === 'rain' ? 100 :
|
||||||
|
type === 'snow' ? 80 :
|
||||||
|
type === 'fog' ? 30 : 50;
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
this.particles.push(this.createParticle(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animateParticles(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
createParticle(type) {
|
||||||
|
return {
|
||||||
|
x: Math.random() * this.canvas.width,
|
||||||
|
y: Math.random() * this.canvas.height,
|
||||||
|
size: type === 'snow' ? Math.random() * 4 + 2 :
|
||||||
|
type === 'fog' ? Math.random() * 100 + 50 :
|
||||||
|
Math.random() * 2 + 1,
|
||||||
|
speed: type === 'snow' ? Math.random() * 2 + 1 :
|
||||||
|
type === 'rain-heavy' ? Math.random() * 15 + 10 :
|
||||||
|
type === 'fog' ? Math.random() * 0.5 + 0.1 :
|
||||||
|
Math.random() * 10 + 5,
|
||||||
|
opacity: type === 'fog' ? Math.random() * 0.3 + 0.1 :
|
||||||
|
Math.random() * 0.5 + 0.3,
|
||||||
|
drift: type === 'snow' ? Math.random() * 2 - 1 :
|
||||||
|
type === 'fog' ? Math.random() * 1 - 0.5 : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
animateParticles(type) {
|
||||||
|
if (!this.isActive) return;
|
||||||
|
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.particles.forEach(particle => {
|
||||||
|
if (type === 'fog') {
|
||||||
|
// Fog: large, blurry circles
|
||||||
|
const gradient = this.ctx.createRadialGradient(
|
||||||
|
particle.x, particle.y, 0,
|
||||||
|
particle.x, particle.y, particle.size
|
||||||
|
);
|
||||||
|
gradient.addColorStop(0, `rgba(180, 180, 190, ${particle.opacity})`);
|
||||||
|
gradient.addColorStop(1, 'rgba(180, 180, 190, 0)');
|
||||||
|
|
||||||
|
this.ctx.fillStyle = gradient;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
particle.x += particle.drift;
|
||||||
|
particle.y += particle.speed * 0.1;
|
||||||
|
} else if (type === 'snow') {
|
||||||
|
// Snow: white circles with drift
|
||||||
|
this.ctx.fillStyle = `rgba(255, 255, 255, ${particle.opacity})`;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
particle.x += particle.drift + Math.sin(particle.y * 0.01) * 0.5;
|
||||||
|
particle.y += particle.speed;
|
||||||
|
} else {
|
||||||
|
// Rain: lines
|
||||||
|
this.ctx.strokeStyle = `rgba(150, 180, 220, ${particle.opacity})`;
|
||||||
|
this.ctx.lineWidth = particle.size;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(particle.x, particle.y);
|
||||||
|
this.ctx.lineTo(particle.x + 1, particle.y + particle.speed * 2);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
particle.y += particle.speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset particles that go off screen
|
||||||
|
if (particle.y > this.canvas.height) {
|
||||||
|
particle.y = -10;
|
||||||
|
particle.x = Math.random() * this.canvas.width;
|
||||||
|
}
|
||||||
|
if (particle.x > this.canvas.width) {
|
||||||
|
particle.x = 0;
|
||||||
|
} else if (particle.x < 0) {
|
||||||
|
particle.x = this.canvas.width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.animationFrame = requestAnimationFrame(() => this.animateParticles(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
stopParticles() {
|
||||||
|
if (this.animationFrame) {
|
||||||
|
cancelAnimationFrame(this.animationFrame);
|
||||||
|
}
|
||||||
|
this.particles = [];
|
||||||
|
if (this.ctx) {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startLightning() {
|
||||||
|
const flash = () => {
|
||||||
|
if (this.currentWeather !== 'storm') return;
|
||||||
|
|
||||||
|
// Random flash
|
||||||
|
if (Math.random() < 0.3) {
|
||||||
|
const flashOverlay = document.createElement('div');
|
||||||
|
flashOverlay.className = 'lightning-flash';
|
||||||
|
document.body.appendChild(flashOverlay);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
flashOverlay.remove();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule next potential flash
|
||||||
|
setTimeout(flash, Math.random() * 5000 + 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
flash();
|
||||||
|
}
|
||||||
|
|
||||||
|
getWeatherInfo() {
|
||||||
|
const weather = this.weatherTypes[this.currentWeather];
|
||||||
|
return {
|
||||||
|
type: this.currentWeather,
|
||||||
|
...weather,
|
||||||
|
timeOfDay: this.timeOfDay
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.isActive = !this.isActive;
|
||||||
|
|
||||||
|
if (this.isActive) {
|
||||||
|
this.weatherContainer.style.display = 'block';
|
||||||
|
this.simulateWeather();
|
||||||
|
} else {
|
||||||
|
this.weatherContainer.style.display = 'none';
|
||||||
|
this.stopParticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.isActive = false;
|
||||||
|
this.stopParticles();
|
||||||
|
if (this.weatherContainer) {
|
||||||
|
this.weatherContainer.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user