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:
Claude
2025-12-28 05:30:13 +00:00
parent 752442967c
commit c7f024948e
10 changed files with 2941 additions and 2 deletions

184
STATUS_V4.md Normal file
View 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

View File

@@ -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>

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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();
}
}
}