mirror of
https://github.com/blackboxprogramming/blackroad-roadworld.git
synced 2026-03-18 00:34:07 -05:00
Add ultimate features for RoadWorld v6.0
- Day/Night cycle with time-based XP bonuses and golden hour events - World events system with random spawning and community challenges - Pet companion system with 8 pets, leveling, and passive bonuses - Treasure hunt quests with GPS waypoint proximity detection - Skill tree with 5 branches and 15 upgradeable abilities - Crafting system for consumables, equipment, and special items - Challenge modes including time trials, survival, and collection - Photo mode with 8 filters, 6 frames, and gallery system
This commit is contained in:
@@ -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 v5.0</title>
|
<title>BlackRoad Earth | RoadWorld v6.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>
|
||||||
@@ -166,6 +166,15 @@
|
|||||||
<button class="ctrl-btn" id="btn-profile" title="Profile">👤</button>
|
<button class="ctrl-btn" id="btn-profile" title="Profile">👤</button>
|
||||||
<button class="ctrl-btn" id="btn-travel" title="Quick Travel">🚀</button>
|
<button class="ctrl-btn" id="btn-travel" title="Quick Travel">🚀</button>
|
||||||
<button class="ctrl-btn" id="btn-keyboard" title="WASD Controls">⌨️</button>
|
<button class="ctrl-btn" id="btn-keyboard" title="WASD Controls">⌨️</button>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<button class="ctrl-btn" id="btn-daynight" title="Day/Night">🌅</button>
|
||||||
|
<button class="ctrl-btn" id="btn-events" title="World Events">🌍</button>
|
||||||
|
<button class="ctrl-btn" id="btn-pet" title="Pets">🐾</button>
|
||||||
|
<button class="ctrl-btn" id="btn-hunt" title="Treasure Hunt">🗺️</button>
|
||||||
|
<button class="ctrl-btn" id="btn-skills" title="Skills">🌳</button>
|
||||||
|
<button class="ctrl-btn" id="btn-craft" title="Crafting">🔨</button>
|
||||||
|
<button class="ctrl-btn" id="btn-challenge" title="Challenges">🏁</button>
|
||||||
|
<button class="ctrl-btn" id="btn-photo" title="Photo Mode">🎨</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Instructions -->
|
<!-- Instructions -->
|
||||||
|
|||||||
924
src/css/main.css
924
src/css/main.css
@@ -2997,3 +2997,927 @@ body[data-time="night"] { }
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
v6.0 ULTIMATE FEATURES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Day/Night Cycle */
|
||||||
|
.day-night-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 50;
|
||||||
|
transition: background 2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
position: fixed;
|
||||||
|
top: 75px;
|
||||||
|
right: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(10, 15, 30, 0.9);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-clock {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-period {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-bonus {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 120px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: rgba(10, 15, 30, 0.95);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 500;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* World Events Panel */
|
||||||
|
.world-events-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 500px;
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-content {
|
||||||
|
max-height: calc(80vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-section h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 3px solid #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card.epic { border-left-color: #9b59b6; }
|
||||||
|
.event-card.legendary { border-left-color: #FFD700; }
|
||||||
|
.event-card.rare { border-left-color: #3498db; }
|
||||||
|
|
||||||
|
.event-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-icon { font-size: 24px; }
|
||||||
|
.event-name { font-weight: 600; flex: 1; }
|
||||||
|
.event-timer {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-progress {
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #0066ff);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) scale(0.8);
|
||||||
|
padding: 20px 30px;
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
|
||||||
|
border: 2px solid #00d4ff;
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 700;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-notification.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pet Panel */
|
||||||
|
.pet-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 450px;
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: calc(80vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-pet-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-pet-icon { font-size: 40px; }
|
||||||
|
.active-pet-info { flex: 1; }
|
||||||
|
.active-pet-name {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-pet-xp-bar {
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-pet-xp-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #0066ff);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-card {
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-card:hover:not(.locked) {
|
||||||
|
border-color: rgba(0, 212, 255, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-card.equipped {
|
||||||
|
border-color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-card.locked { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.pet-card-icon { font-size: 28px; }
|
||||||
|
.pet-card-name { font-size: 11px; margin-top: 5px; }
|
||||||
|
|
||||||
|
.pet-marker {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(10, 15, 30, 0.9);
|
||||||
|
border: 2px solid #00d4ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-marker.legendary { border-color: #FFD700; }
|
||||||
|
.pet-marker.epic { border-color: #9b59b6; }
|
||||||
|
|
||||||
|
/* Treasure Hunt Panel */
|
||||||
|
.treasure-hunt-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 500px;
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: calc(80vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 3px solid #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-card.legendary { border-left-color: #FFD700; }
|
||||||
|
.hunt-card.epic { border-left-color: #9b59b6; }
|
||||||
|
|
||||||
|
.hunt-card-icon { font-size: 32px; }
|
||||||
|
.hunt-card-info { flex: 1; }
|
||||||
|
.hunt-card-name { font-weight: 600; margin-bottom: 4px; }
|
||||||
|
.hunt-card-desc { font-size: 12px; opacity: 0.7; }
|
||||||
|
|
||||||
|
.hunt-start-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #0066ff);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-marker {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: linear-gradient(135deg, #FFD700, #ff6b35);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
position: relative;
|
||||||
|
animation: huntPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-marker.visited {
|
||||||
|
background: #27ae60;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-marker.current {
|
||||||
|
animation: huntGlow 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes huntPulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes huntGlow {
|
||||||
|
0%, 100% { box-shadow: 0 0 10px rgba(255, 215, 0, 0.5); }
|
||||||
|
50% { box-shadow: 0 0 25px rgba(255, 215, 0, 0.8); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-complete-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-complete-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hunt-complete-content .hunt-complete-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: bounceIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounceIn {
|
||||||
|
0% { transform: scale(0); }
|
||||||
|
50% { transform: scale(1.2); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skill Tree Panel */
|
||||||
|
.skill-tree-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 600px;
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 85vh;
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: calc(85vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-points-display {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-points-value {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-branch {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
border-left: 3px solid var(--branch-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-name {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--branch-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-skills {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-node {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: not-allowed;
|
||||||
|
position: relative;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-node.available {
|
||||||
|
border-color: var(--branch-color);
|
||||||
|
opacity: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
animation: skillAvailable 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-node.unlocked {
|
||||||
|
background: var(--branch-color);
|
||||||
|
border-color: var(--branch-color);
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 15px var(--branch-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skillAvailable {
|
||||||
|
0%, 100% { box-shadow: 0 0 5px var(--branch-color); }
|
||||||
|
50% { box-shadow: 0 0 15px var(--branch-color); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(10, 15, 30, 0.98);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
min-width: 150px;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-node:hover .skill-tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Crafting Panel */
|
||||||
|
.crafting-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 550px;
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 85vh;
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: calc(85vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-resources {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-tab.active {
|
||||||
|
background: rgba(0, 212, 255, 0.2);
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-card.locked { opacity: 0.6; }
|
||||||
|
.recipe-icon { font-size: 32px; }
|
||||||
|
.recipe-info { flex: 1; }
|
||||||
|
.recipe-name { font-weight: 600; }
|
||||||
|
.recipe-desc { font-size: 12px; opacity: 0.7; }
|
||||||
|
|
||||||
|
.recipe-ingredients {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-ingredients .enough { color: #27ae60; }
|
||||||
|
.recipe-ingredients .need { color: #e74c3c; }
|
||||||
|
|
||||||
|
.craft-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: linear-gradient(135deg, #FFD700, #ff6b35);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.craft-btn:disabled {
|
||||||
|
background: #555;
|
||||||
|
color: #888;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Challenge Modes Panel */
|
||||||
|
.challenge-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 500px;
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: calc(80vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-tab.active {
|
||||||
|
background: rgba(0, 212, 255, 0.2);
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 3px solid #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-card.hard { border-left-color: #e74c3c; }
|
||||||
|
.challenge-card.medium { border-left-color: #f39c12; }
|
||||||
|
.challenge-card.easy { border-left-color: #27ae60; }
|
||||||
|
|
||||||
|
.challenge-card-icon { font-size: 28px; }
|
||||||
|
.challenge-card-info { flex: 1; }
|
||||||
|
.challenge-card-name { font-weight: 600; }
|
||||||
|
.challenge-card-desc { font-size: 12px; opacity: 0.7; }
|
||||||
|
|
||||||
|
.challenge-card-difficulty {
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-hud {
|
||||||
|
position: fixed;
|
||||||
|
top: 120px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(10, 15, 30, 0.95);
|
||||||
|
border: 2px solid #00d4ff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
z-index: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-hud-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-hud-timer {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-hud-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-hud-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, #0066ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-complete-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-medal {
|
||||||
|
font-size: 80px;
|
||||||
|
animation: bounceIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Photo Mode Panel */
|
||||||
|
.photo-mode-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
z-index: 800;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-mode-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 25px;
|
||||||
|
background: rgba(10, 15, 30, 0.9);
|
||||||
|
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-mode-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-controls {
|
||||||
|
width: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-section h4 {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-grid, .frame-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn, .frame-btn {
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.selected, .frame-btn.selected {
|
||||||
|
border-color: #00d4ff;
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-icon, .frame-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-name, .frame-name {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-options label {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-preview-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-preview {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
min-width: 300px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: rgba(0, 212, 255, 0.2);
|
||||||
|
border: 1px solid rgba(0, 212, 255, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-btn.capture {
|
||||||
|
background: linear-gradient(135deg, #00d4ff, #0066ff);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery-section {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
width: 80px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-fullscreen img {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive for v6.0 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.time-display {
|
||||||
|
right: 20px;
|
||||||
|
top: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world-events-panel,
|
||||||
|
.pet-panel,
|
||||||
|
.treasure-hunt-panel,
|
||||||
|
.skill-tree-panel,
|
||||||
|
.crafting-panel,
|
||||||
|
.challenge-panel {
|
||||||
|
width: 95vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pet-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-grid, .frame-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-mode-content {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-controls {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
546
src/js/challengeModes.js
Normal file
546
src/js/challengeModes.js
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
// Challenge Modes for RoadWorld
|
||||||
|
// Time trials, races, and special challenges
|
||||||
|
|
||||||
|
export class ChallengeModes {
|
||||||
|
constructor(mapManager, gameEngine, storageManager) {
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
this.activeChallenge = null;
|
||||||
|
this.challengeRecords = {};
|
||||||
|
this.panelElement = null;
|
||||||
|
this.hudElement = null;
|
||||||
|
this.isVisible = false;
|
||||||
|
|
||||||
|
// Challenge definitions
|
||||||
|
this.challenges = {
|
||||||
|
// Time Trials
|
||||||
|
speedDash: {
|
||||||
|
id: 'speedDash',
|
||||||
|
name: 'Speed Dash',
|
||||||
|
icon: '⚡',
|
||||||
|
type: 'timeTrial',
|
||||||
|
description: 'Collect 20 items as fast as possible!',
|
||||||
|
target: 20,
|
||||||
|
targetType: 'items',
|
||||||
|
difficulty: 'easy',
|
||||||
|
rewards: {
|
||||||
|
bronze: { time: 120, xp: 500, items: { stars: 5 } },
|
||||||
|
silver: { time: 60, xp: 1000, items: { gems: 3 } },
|
||||||
|
gold: { time: 30, xp: 2000, items: { trophies: 1 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
marathonMaster: {
|
||||||
|
id: 'marathonMaster',
|
||||||
|
name: 'Marathon Master',
|
||||||
|
icon: '🏃',
|
||||||
|
type: 'timeTrial',
|
||||||
|
description: 'Travel 5km as fast as possible!',
|
||||||
|
target: 5000, // meters
|
||||||
|
targetType: 'distance',
|
||||||
|
difficulty: 'medium',
|
||||||
|
rewards: {
|
||||||
|
bronze: { time: 600, xp: 800, items: { stars: 10 } },
|
||||||
|
silver: { time: 300, xp: 1500, items: { gems: 5 } },
|
||||||
|
gold: { time: 180, xp: 3000, items: { trophies: 2 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xpExpress: {
|
||||||
|
id: 'xpExpress',
|
||||||
|
name: 'XP Express',
|
||||||
|
icon: '✨',
|
||||||
|
type: 'timeTrial',
|
||||||
|
description: 'Earn 1000 XP as fast as possible!',
|
||||||
|
target: 1000,
|
||||||
|
targetType: 'xp',
|
||||||
|
difficulty: 'hard',
|
||||||
|
rewards: {
|
||||||
|
bronze: { time: 300, xp: 1000, items: { stars: 15 } },
|
||||||
|
silver: { time: 180, xp: 2000, items: { gems: 8 } },
|
||||||
|
gold: { time: 90, xp: 4000, items: { trophies: 3 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Survival Challenges
|
||||||
|
comboKing: {
|
||||||
|
id: 'comboKing',
|
||||||
|
name: 'Combo King',
|
||||||
|
icon: '🔥',
|
||||||
|
type: 'survival',
|
||||||
|
description: 'Reach a 50x combo without breaking!',
|
||||||
|
target: 50,
|
||||||
|
targetType: 'combo',
|
||||||
|
difficulty: 'hard',
|
||||||
|
rewards: {
|
||||||
|
complete: { xp: 2500, items: { gems: 10 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nightSurvivor: {
|
||||||
|
id: 'nightSurvivor',
|
||||||
|
name: 'Night Survivor',
|
||||||
|
icon: '🌙',
|
||||||
|
type: 'survival',
|
||||||
|
description: 'Collect 30 items during night time!',
|
||||||
|
target: 30,
|
||||||
|
targetType: 'nightItems',
|
||||||
|
difficulty: 'medium',
|
||||||
|
rewards: {
|
||||||
|
complete: { xp: 1500, items: { gems: 5, keys: 2 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Collection Challenges
|
||||||
|
gemCollector: {
|
||||||
|
id: 'gemCollector',
|
||||||
|
name: 'Gem Collector',
|
||||||
|
icon: '💎',
|
||||||
|
type: 'collection',
|
||||||
|
description: 'Collect 10 gems in one session!',
|
||||||
|
target: 10,
|
||||||
|
targetType: 'gems',
|
||||||
|
difficulty: 'medium',
|
||||||
|
rewards: {
|
||||||
|
complete: { xp: 1200, items: { gems: 5 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trophyHunter: {
|
||||||
|
id: 'trophyHunter',
|
||||||
|
name: 'Trophy Hunter',
|
||||||
|
icon: '🏆',
|
||||||
|
type: 'collection',
|
||||||
|
description: 'Collect 5 trophies in one session!',
|
||||||
|
target: 5,
|
||||||
|
targetType: 'trophies',
|
||||||
|
difficulty: 'hard',
|
||||||
|
rewards: {
|
||||||
|
complete: { xp: 2000, items: { trophies: 3 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Special Challenges
|
||||||
|
worldTour: {
|
||||||
|
id: 'worldTour',
|
||||||
|
name: 'World Tour',
|
||||||
|
icon: '🌍',
|
||||||
|
type: 'exploration',
|
||||||
|
description: 'Visit 3 continents in 10 minutes!',
|
||||||
|
target: 3,
|
||||||
|
targetType: 'continents',
|
||||||
|
timeLimit: 600,
|
||||||
|
difficulty: 'hard',
|
||||||
|
rewards: {
|
||||||
|
complete: { xp: 3000, items: { trophies: 2, gems: 10 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
photoJourney: {
|
||||||
|
id: 'photoJourney',
|
||||||
|
name: 'Photo Journey',
|
||||||
|
icon: '📸',
|
||||||
|
type: 'special',
|
||||||
|
description: 'Take 5 screenshots at famous landmarks!',
|
||||||
|
target: 5,
|
||||||
|
targetType: 'photos',
|
||||||
|
difficulty: 'easy',
|
||||||
|
rewards: {
|
||||||
|
complete: { xp: 1000, items: { stars: 20 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.createHUD();
|
||||||
|
this.loadRecords();
|
||||||
|
|
||||||
|
console.log('🏁 Challenge Modes initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'challenge-panel ui-overlay';
|
||||||
|
this.panelElement.id = 'challenge-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>🏁 Challenges</span>
|
||||||
|
<button class="panel-close" id="challenge-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content challenge-content">
|
||||||
|
<div class="challenge-tabs">
|
||||||
|
<button class="challenge-tab active" data-type="timeTrial">⚡ Time Trials</button>
|
||||||
|
<button class="challenge-tab" data-type="survival">💪 Survival</button>
|
||||||
|
<button class="challenge-tab" data-type="collection">📦 Collection</button>
|
||||||
|
<button class="challenge-tab" data-type="special">✨ Special</button>
|
||||||
|
</div>
|
||||||
|
<div class="challenge-list" id="challenge-list"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
createHUD() {
|
||||||
|
this.hudElement = document.createElement('div');
|
||||||
|
this.hudElement.className = 'challenge-hud';
|
||||||
|
this.hudElement.id = 'challenge-hud';
|
||||||
|
this.hudElement.style.display = 'none';
|
||||||
|
document.body.appendChild(this.hudElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('challenge-close').addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.panelElement.querySelectorAll('.challenge-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
this.panelElement.querySelectorAll('.challenge-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
e.target.classList.add('active');
|
||||||
|
this.renderChallenges(e.target.dataset.type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRecords() {
|
||||||
|
const saved = this.storageManager.data.challengeRecords || {};
|
||||||
|
this.challengeRecords = saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRecords() {
|
||||||
|
this.storageManager.data.challengeRecords = this.challengeRecords;
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
startChallenge(challengeId) {
|
||||||
|
const challenge = this.challenges[challengeId];
|
||||||
|
if (!challenge) return;
|
||||||
|
|
||||||
|
if (this.activeChallenge) {
|
||||||
|
this.showNotification('Complete or abandon current challenge first!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeChallenge = {
|
||||||
|
...challenge,
|
||||||
|
startTime: Date.now(),
|
||||||
|
progress: 0,
|
||||||
|
startXP: this.gameEngine.player.xp,
|
||||||
|
startDistance: this.gameEngine.player.stats.distanceTraveled,
|
||||||
|
visitedContinents: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.showHUD();
|
||||||
|
this.startUpdateLoop();
|
||||||
|
this.hide();
|
||||||
|
|
||||||
|
this.showNotification(`🏁 Challenge started: ${challenge.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
abandonChallenge() {
|
||||||
|
if (!this.activeChallenge) return;
|
||||||
|
|
||||||
|
this.stopUpdateLoop();
|
||||||
|
this.hideHUD();
|
||||||
|
this.activeChallenge = null;
|
||||||
|
|
||||||
|
this.showNotification('Challenge abandoned');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(type, amount = 1) {
|
||||||
|
if (!this.activeChallenge) return;
|
||||||
|
|
||||||
|
const c = this.activeChallenge;
|
||||||
|
|
||||||
|
switch (c.targetType) {
|
||||||
|
case 'items':
|
||||||
|
if (type === 'item') c.progress += amount;
|
||||||
|
break;
|
||||||
|
case 'distance':
|
||||||
|
const currentDist = this.gameEngine.player.stats.distanceTraveled;
|
||||||
|
c.progress = currentDist - c.startDistance;
|
||||||
|
break;
|
||||||
|
case 'xp':
|
||||||
|
const currentXP = this.gameEngine.player.xp;
|
||||||
|
c.progress = currentXP - c.startXP;
|
||||||
|
break;
|
||||||
|
case 'combo':
|
||||||
|
if (type === 'combo' && amount > c.progress) c.progress = amount;
|
||||||
|
break;
|
||||||
|
case 'gems':
|
||||||
|
case 'trophies':
|
||||||
|
case 'stars':
|
||||||
|
if (type === c.targetType) c.progress += amount;
|
||||||
|
break;
|
||||||
|
case 'nightItems':
|
||||||
|
// Would check if night time
|
||||||
|
if (type === 'item') c.progress += amount;
|
||||||
|
break;
|
||||||
|
case 'photos':
|
||||||
|
if (type === 'screenshot') c.progress += amount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateHUD();
|
||||||
|
this.checkCompletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCompletion() {
|
||||||
|
if (!this.activeChallenge) return;
|
||||||
|
|
||||||
|
const c = this.activeChallenge;
|
||||||
|
|
||||||
|
// Check time limit for timed challenges
|
||||||
|
if (c.timeLimit) {
|
||||||
|
const elapsed = (Date.now() - c.startTime) / 1000;
|
||||||
|
if (elapsed > c.timeLimit) {
|
||||||
|
this.failChallenge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check target reached
|
||||||
|
if (c.progress >= c.target) {
|
||||||
|
this.completeChallenge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completeChallenge() {
|
||||||
|
const c = this.activeChallenge;
|
||||||
|
const elapsed = (Date.now() - c.startTime) / 1000;
|
||||||
|
|
||||||
|
// Determine medal for time trials
|
||||||
|
let medal = null;
|
||||||
|
let rewards = null;
|
||||||
|
|
||||||
|
if (c.type === 'timeTrial') {
|
||||||
|
if (elapsed <= c.rewards.gold.time) {
|
||||||
|
medal = 'gold';
|
||||||
|
rewards = c.rewards.gold;
|
||||||
|
} else if (elapsed <= c.rewards.silver.time) {
|
||||||
|
medal = 'silver';
|
||||||
|
rewards = c.rewards.silver;
|
||||||
|
} else if (elapsed <= c.rewards.bronze.time) {
|
||||||
|
medal = 'bronze';
|
||||||
|
rewards = c.rewards.bronze;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
medal = 'complete';
|
||||||
|
rewards = c.rewards.complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save record
|
||||||
|
const record = this.challengeRecords[c.id] || {};
|
||||||
|
if (!record.bestTime || elapsed < record.bestTime) {
|
||||||
|
record.bestTime = elapsed;
|
||||||
|
record.medal = medal;
|
||||||
|
}
|
||||||
|
this.challengeRecords[c.id] = record;
|
||||||
|
this.saveRecords();
|
||||||
|
|
||||||
|
// Award rewards
|
||||||
|
if (rewards && this.gameEngine) {
|
||||||
|
this.gameEngine.addXP(rewards.xp, 'challenge');
|
||||||
|
|
||||||
|
if (rewards.items) {
|
||||||
|
Object.entries(rewards.items).forEach(([type, count]) => {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.gameEngine.collectItem({ type, rarity: 'challenge' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showCompletionScreen(c, elapsed, medal, rewards);
|
||||||
|
|
||||||
|
this.stopUpdateLoop();
|
||||||
|
this.hideHUD();
|
||||||
|
this.activeChallenge = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
failChallenge() {
|
||||||
|
const c = this.activeChallenge;
|
||||||
|
|
||||||
|
this.showNotification(`⏱️ Time's up! Challenge failed.`);
|
||||||
|
|
||||||
|
this.stopUpdateLoop();
|
||||||
|
this.hideHUD();
|
||||||
|
this.activeChallenge = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
showCompletionScreen(challenge, time, medal, rewards) {
|
||||||
|
const medalIcons = { gold: '🥇', silver: '🥈', bronze: '🥉', complete: '✅' };
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
const seconds = Math.floor(time % 60);
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'challenge-complete-overlay';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="challenge-complete-content">
|
||||||
|
<div class="challenge-medal">${medalIcons[medal] || '🏁'}</div>
|
||||||
|
<div class="challenge-complete-title">Challenge Complete!</div>
|
||||||
|
<div class="challenge-complete-name">${challenge.name}</div>
|
||||||
|
<div class="challenge-time">${minutes}:${seconds.toString().padStart(2, '0')}</div>
|
||||||
|
${rewards ? `
|
||||||
|
<div class="challenge-rewards">
|
||||||
|
<div class="reward-xp">+${rewards.xp} XP</div>
|
||||||
|
${rewards.items ? `
|
||||||
|
<div class="reward-items">
|
||||||
|
${Object.entries(rewards.items).map(([type, count]) =>
|
||||||
|
`<span>${this.getItemIcon(type)} x${count}</span>`
|
||||||
|
).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<button class="challenge-complete-btn" id="challenge-complete-close">Continue</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
document.getElementById('challenge-complete-close').addEventListener('click', () => {
|
||||||
|
overlay.classList.add('fade-out');
|
||||||
|
setTimeout(() => overlay.remove(), 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemIcon(type) {
|
||||||
|
const icons = { stars: '⭐', gems: '💎', trophies: '🏆', keys: '🗝️' };
|
||||||
|
return icons[type] || '✨';
|
||||||
|
}
|
||||||
|
|
||||||
|
startUpdateLoop() {
|
||||||
|
this.updateInterval = setInterval(() => {
|
||||||
|
this.updateHUD();
|
||||||
|
this.checkCompletion();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopUpdateLoop() {
|
||||||
|
if (this.updateInterval) {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
this.updateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showHUD() {
|
||||||
|
if (!this.activeChallenge) return;
|
||||||
|
|
||||||
|
const c = this.activeChallenge;
|
||||||
|
this.hudElement.style.display = 'block';
|
||||||
|
this.updateHUD();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideHUD() {
|
||||||
|
this.hudElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHUD() {
|
||||||
|
if (!this.activeChallenge) return;
|
||||||
|
|
||||||
|
const c = this.activeChallenge;
|
||||||
|
const elapsed = (Date.now() - c.startTime) / 1000;
|
||||||
|
const minutes = Math.floor(elapsed / 60);
|
||||||
|
const seconds = Math.floor(elapsed % 60);
|
||||||
|
const progressPercent = Math.min(100, (c.progress / c.target) * 100);
|
||||||
|
|
||||||
|
this.hudElement.innerHTML = `
|
||||||
|
<div class="challenge-hud-header">
|
||||||
|
<span class="challenge-hud-icon">${c.icon}</span>
|
||||||
|
<span class="challenge-hud-name">${c.name}</span>
|
||||||
|
<button class="challenge-hud-abandon" id="hud-abandon">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="challenge-hud-timer">${minutes}:${seconds.toString().padStart(2, '0')}</div>
|
||||||
|
<div class="challenge-hud-progress">
|
||||||
|
<div class="challenge-hud-bar">
|
||||||
|
<div class="challenge-hud-fill" style="width: ${progressPercent}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="challenge-hud-text">${Math.floor(c.progress)}/${c.target}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('hud-abandon')?.addEventListener('click', () => {
|
||||||
|
if (confirm('Abandon this challenge?')) {
|
||||||
|
this.abandonChallenge();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChallenges(type) {
|
||||||
|
const listEl = document.getElementById('challenge-list');
|
||||||
|
if (!listEl) return;
|
||||||
|
|
||||||
|
const filtered = Object.values(this.challenges).filter(c =>
|
||||||
|
c.type === type || (type === 'special' && (c.type === 'exploration' || c.type === 'special'))
|
||||||
|
);
|
||||||
|
|
||||||
|
listEl.innerHTML = filtered.map(challenge => {
|
||||||
|
const record = this.challengeRecords[challenge.id];
|
||||||
|
const medalIcons = { gold: '🥇', silver: '🥈', bronze: '🥉', complete: '✅' };
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="challenge-card ${challenge.difficulty}" data-challenge="${challenge.id}">
|
||||||
|
<div class="challenge-card-icon">${challenge.icon}</div>
|
||||||
|
<div class="challenge-card-info">
|
||||||
|
<div class="challenge-card-name">${challenge.name}</div>
|
||||||
|
<div class="challenge-card-desc">${challenge.description}</div>
|
||||||
|
<div class="challenge-card-difficulty">${challenge.difficulty.toUpperCase()}</div>
|
||||||
|
</div>
|
||||||
|
<div class="challenge-card-record">
|
||||||
|
${record ? `
|
||||||
|
<div class="record-medal">${medalIcons[record.medal] || ''}</div>
|
||||||
|
<div class="record-time">${Math.floor(record.bestTime)}s</div>
|
||||||
|
` : `
|
||||||
|
<div class="no-record">Not attempted</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<button class="challenge-start-btn">Start</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
listEl.querySelectorAll('.challenge-start-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const challengeId = e.target.closest('.challenge-card').dataset.challenge;
|
||||||
|
this.startChallenge(challengeId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
this.renderChallenges('timeTrial');
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isVisible = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
537
src/js/craftingSystem.js
Normal file
537
src/js/craftingSystem.js
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
// Crafting System for RoadWorld
|
||||||
|
// Combine collected items to create special gear and consumables
|
||||||
|
|
||||||
|
export class CraftingSystem {
|
||||||
|
constructor(gameEngine, storageManager) {
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
this.panelElement = null;
|
||||||
|
this.isVisible = false;
|
||||||
|
|
||||||
|
// Crafting recipes
|
||||||
|
this.recipes = {
|
||||||
|
// Consumables
|
||||||
|
xpPotion: {
|
||||||
|
id: 'xpPotion',
|
||||||
|
name: 'XP Potion',
|
||||||
|
icon: '🧪',
|
||||||
|
category: 'consumable',
|
||||||
|
description: 'Grants 500 bonus XP instantly!',
|
||||||
|
ingredients: { stars: 10, gems: 2 },
|
||||||
|
effect: { type: 'instantXP', value: 500 },
|
||||||
|
craftTime: 0
|
||||||
|
},
|
||||||
|
luckyCharm: {
|
||||||
|
id: 'luckyCharm',
|
||||||
|
name: 'Lucky Charm',
|
||||||
|
icon: '🍀',
|
||||||
|
category: 'consumable',
|
||||||
|
description: '+50% rare item chance for 5 minutes',
|
||||||
|
ingredients: { gems: 5, stars: 5 },
|
||||||
|
effect: { type: 'rareBoost', value: 1.5, duration: 300000 },
|
||||||
|
craftTime: 0
|
||||||
|
},
|
||||||
|
speedBoost: {
|
||||||
|
id: 'speedBoost',
|
||||||
|
name: 'Speed Elixir',
|
||||||
|
icon: '⚡',
|
||||||
|
category: 'consumable',
|
||||||
|
description: '+30% movement speed for 3 minutes',
|
||||||
|
ingredients: { stars: 8, keys: 1 },
|
||||||
|
effect: { type: 'speedBoost', value: 1.3, duration: 180000 },
|
||||||
|
craftTime: 0
|
||||||
|
},
|
||||||
|
magnetOrb: {
|
||||||
|
id: 'magnetOrb',
|
||||||
|
name: 'Magnet Orb',
|
||||||
|
icon: '🔮',
|
||||||
|
category: 'consumable',
|
||||||
|
description: 'Auto-collect items in 100m radius',
|
||||||
|
ingredients: { gems: 8, trophies: 1 },
|
||||||
|
effect: { type: 'magnetPulse', value: 100 },
|
||||||
|
craftTime: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// Equipment
|
||||||
|
explorerBadge: {
|
||||||
|
id: 'explorerBadge',
|
||||||
|
name: 'Explorer Badge',
|
||||||
|
icon: '🎖️',
|
||||||
|
category: 'equipment',
|
||||||
|
description: '+10% XP permanently while equipped',
|
||||||
|
ingredients: { trophies: 3, gems: 10, stars: 20 },
|
||||||
|
effect: { type: 'xpBoost', value: 1.1 },
|
||||||
|
craftTime: 60000 // 1 minute
|
||||||
|
},
|
||||||
|
treasureCompass: {
|
||||||
|
id: 'treasureCompass',
|
||||||
|
name: 'Treasure Compass',
|
||||||
|
icon: '🧭',
|
||||||
|
category: 'equipment',
|
||||||
|
description: 'Shows nearby rare items on map',
|
||||||
|
ingredients: { keys: 5, gems: 15 },
|
||||||
|
effect: { type: 'treasureRadar', value: 200 },
|
||||||
|
craftTime: 120000
|
||||||
|
},
|
||||||
|
goldenBoots: {
|
||||||
|
id: 'goldenBoots',
|
||||||
|
name: 'Golden Boots',
|
||||||
|
icon: '👢',
|
||||||
|
category: 'equipment',
|
||||||
|
description: '+20% movement speed, leave golden trail',
|
||||||
|
ingredients: { trophies: 5, gems: 25, stars: 50 },
|
||||||
|
effect: { type: 'goldenSpeed', value: 1.2 },
|
||||||
|
craftTime: 300000 // 5 minutes
|
||||||
|
},
|
||||||
|
starCape: {
|
||||||
|
id: 'starCape',
|
||||||
|
name: 'Star Cape',
|
||||||
|
icon: '🌟',
|
||||||
|
category: 'equipment',
|
||||||
|
description: 'Stars give 2x XP',
|
||||||
|
ingredients: { stars: 100, gems: 20 },
|
||||||
|
effect: { type: 'starBonus', value: 2 },
|
||||||
|
craftTime: 180000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Special items
|
||||||
|
petEgg: {
|
||||||
|
id: 'petEgg',
|
||||||
|
name: 'Mystery Egg',
|
||||||
|
icon: '🥚',
|
||||||
|
category: 'special',
|
||||||
|
description: 'Hatches into a random pet!',
|
||||||
|
ingredients: { gems: 30, trophies: 3, keys: 3 },
|
||||||
|
effect: { type: 'petUnlock', value: 'random' },
|
||||||
|
craftTime: 600000 // 10 minutes
|
||||||
|
},
|
||||||
|
warpCrystal: {
|
||||||
|
id: 'warpCrystal',
|
||||||
|
name: 'Warp Crystal',
|
||||||
|
icon: '💎',
|
||||||
|
category: 'special',
|
||||||
|
description: 'Instant teleport, no cooldown!',
|
||||||
|
ingredients: { gems: 20, keys: 5 },
|
||||||
|
effect: { type: 'instantTravel', value: 1 },
|
||||||
|
craftTime: 60000
|
||||||
|
},
|
||||||
|
experienceOrb: {
|
||||||
|
id: 'experienceOrb',
|
||||||
|
name: 'Experience Orb',
|
||||||
|
icon: '💫',
|
||||||
|
category: 'special',
|
||||||
|
description: 'Grants a full level instantly!',
|
||||||
|
ingredients: { trophies: 10, gems: 50, stars: 100 },
|
||||||
|
effect: { type: 'levelUp', value: 1 },
|
||||||
|
craftTime: 900000 // 15 minutes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Currently crafting
|
||||||
|
this.craftingQueue = [];
|
||||||
|
this.equippedItems = [];
|
||||||
|
this.craftedItems = {};
|
||||||
|
|
||||||
|
this.updateInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.loadProgress();
|
||||||
|
this.startUpdateLoop();
|
||||||
|
|
||||||
|
console.log('🔨 Crafting System initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'crafting-panel ui-overlay';
|
||||||
|
this.panelElement.id = 'crafting-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>🔨 Crafting</span>
|
||||||
|
<button class="panel-close" id="crafting-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content crafting-content">
|
||||||
|
<div class="crafting-resources" id="crafting-resources"></div>
|
||||||
|
<div class="crafting-queue" id="crafting-queue"></div>
|
||||||
|
<div class="crafting-tabs">
|
||||||
|
<button class="craft-tab active" data-category="consumable">🧪 Consumables</button>
|
||||||
|
<button class="craft-tab" data-category="equipment">⚔️ Equipment</button>
|
||||||
|
<button class="craft-tab" data-category="special">✨ Special</button>
|
||||||
|
</div>
|
||||||
|
<div class="crafting-recipes" id="crafting-recipes"></div>
|
||||||
|
<div class="equipped-items" id="equipped-items"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('crafting-close').addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.panelElement.querySelectorAll('.craft-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', (e) => {
|
||||||
|
this.panelElement.querySelectorAll('.craft-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
e.target.classList.add('active');
|
||||||
|
this.renderRecipes(e.target.dataset.category);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProgress() {
|
||||||
|
const saved = this.storageManager.data.crafting || {};
|
||||||
|
this.craftingQueue = saved.queue || [];
|
||||||
|
this.equippedItems = saved.equipped || [];
|
||||||
|
this.craftedItems = saved.crafted || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProgress() {
|
||||||
|
this.storageManager.data.crafting = {
|
||||||
|
queue: this.craftingQueue,
|
||||||
|
equipped: this.equippedItems,
|
||||||
|
crafted: this.craftedItems
|
||||||
|
};
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
startUpdateLoop() {
|
||||||
|
this.updateInterval = setInterval(() => {
|
||||||
|
this.updateCraftingQueue();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
canCraft(recipeId) {
|
||||||
|
const recipe = this.recipes[recipeId];
|
||||||
|
if (!recipe) return false;
|
||||||
|
|
||||||
|
const inventory = this.gameEngine.getInventorySummary();
|
||||||
|
|
||||||
|
return Object.entries(recipe.ingredients).every(([item, count]) => {
|
||||||
|
return (inventory[item] || 0) >= count;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
craft(recipeId) {
|
||||||
|
const recipe = this.recipes[recipeId];
|
||||||
|
if (!recipe || !this.canCraft(recipeId)) return false;
|
||||||
|
|
||||||
|
// Consume ingredients
|
||||||
|
const inventory = this.gameEngine.player.inventory;
|
||||||
|
Object.entries(recipe.ingredients).forEach(([item, count]) => {
|
||||||
|
inventory[item] = (inventory[item] || 0) - count;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recipe.craftTime > 0) {
|
||||||
|
// Add to queue
|
||||||
|
this.craftingQueue.push({
|
||||||
|
recipeId,
|
||||||
|
startTime: Date.now(),
|
||||||
|
endTime: Date.now() + recipe.craftTime
|
||||||
|
});
|
||||||
|
this.showNotification(`🔨 Crafting ${recipe.name}...`);
|
||||||
|
} else {
|
||||||
|
// Instant craft
|
||||||
|
this.onCraftComplete(recipeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gameEngine.savePlayer();
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderPanel();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCraftingQueue() {
|
||||||
|
const now = Date.now();
|
||||||
|
const completed = [];
|
||||||
|
|
||||||
|
this.craftingQueue = this.craftingQueue.filter(item => {
|
||||||
|
if (now >= item.endTime) {
|
||||||
|
completed.push(item.recipeId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
completed.forEach(recipeId => {
|
||||||
|
this.onCraftComplete(recipeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.renderQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCraftComplete(recipeId) {
|
||||||
|
const recipe = this.recipes[recipeId];
|
||||||
|
|
||||||
|
// Add to crafted items
|
||||||
|
this.craftedItems[recipeId] = (this.craftedItems[recipeId] || 0) + 1;
|
||||||
|
|
||||||
|
this.showNotification(`✨ Crafted: ${recipe.name}!`);
|
||||||
|
|
||||||
|
// Apply immediate effects for consumables
|
||||||
|
if (recipe.category === 'consumable') {
|
||||||
|
this.useItem(recipeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
useItem(itemId) {
|
||||||
|
const recipe = this.recipes[itemId];
|
||||||
|
if (!recipe || (this.craftedItems[itemId] || 0) <= 0) return false;
|
||||||
|
|
||||||
|
const effect = recipe.effect;
|
||||||
|
|
||||||
|
switch (effect.type) {
|
||||||
|
case 'instantXP':
|
||||||
|
this.gameEngine.addXP(effect.value, 'craft');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'rareBoost':
|
||||||
|
case 'speedBoost':
|
||||||
|
// Apply temporary buff
|
||||||
|
this.applyTemporaryEffect(effect);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'magnetPulse':
|
||||||
|
this.triggerMagnetPulse(effect.value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'levelUp':
|
||||||
|
this.gameEngine.addXP(this.gameEngine.player.xpToNextLevel, 'craft');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'instantTravel':
|
||||||
|
// Grant instant travel token
|
||||||
|
this.showNotification('💎 Warp Crystal ready! Next quick travel is instant.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume item
|
||||||
|
this.craftedItems[itemId]--;
|
||||||
|
if (this.craftedItems[itemId] <= 0) {
|
||||||
|
delete this.craftedItems[itemId];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveProgress();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
equipItem(itemId) {
|
||||||
|
const recipe = this.recipes[itemId];
|
||||||
|
if (!recipe || recipe.category !== 'equipment') return false;
|
||||||
|
if ((this.craftedItems[itemId] || 0) <= 0) return false;
|
||||||
|
|
||||||
|
// Can only have 3 items equipped
|
||||||
|
if (this.equippedItems.length >= 3 && !this.equippedItems.includes(itemId)) {
|
||||||
|
this.showNotification('Unequip an item first! (max 3)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.equippedItems.includes(itemId)) {
|
||||||
|
// Unequip
|
||||||
|
this.equippedItems = this.equippedItems.filter(i => i !== itemId);
|
||||||
|
} else {
|
||||||
|
// Equip
|
||||||
|
this.equippedItems.push(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderPanel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTemporaryEffect(effect) {
|
||||||
|
// Temporary effects handled by power-ups manager integration
|
||||||
|
this.showNotification(`⚡ ${effect.type} active for ${effect.duration / 1000}s!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerMagnetPulse(range) {
|
||||||
|
// Would collect all items within range
|
||||||
|
this.showNotification(`🔮 Magnet pulse! Collecting items in ${range}m radius...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEquippedBonus(type) {
|
||||||
|
let bonus = 1;
|
||||||
|
|
||||||
|
this.equippedItems.forEach(itemId => {
|
||||||
|
const recipe = this.recipes[itemId];
|
||||||
|
if (recipe?.effect.type === type) {
|
||||||
|
bonus *= recipe.effect.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPanel() {
|
||||||
|
this.renderResources();
|
||||||
|
this.renderQueue();
|
||||||
|
this.renderRecipes('consumable');
|
||||||
|
this.renderEquipped();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResources() {
|
||||||
|
const resourcesEl = document.getElementById('crafting-resources');
|
||||||
|
if (!resourcesEl) return;
|
||||||
|
|
||||||
|
const inventory = this.gameEngine.getInventorySummary();
|
||||||
|
resourcesEl.innerHTML = `
|
||||||
|
<div class="resource-item"><span>⭐</span>${inventory.stars}</div>
|
||||||
|
<div class="resource-item"><span>💎</span>${inventory.gems}</div>
|
||||||
|
<div class="resource-item"><span>🏆</span>${inventory.trophies}</div>
|
||||||
|
<div class="resource-item"><span>🗝️</span>${inventory.keys}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderQueue() {
|
||||||
|
const queueEl = document.getElementById('crafting-queue');
|
||||||
|
if (!queueEl) return;
|
||||||
|
|
||||||
|
if (this.craftingQueue.length === 0) {
|
||||||
|
queueEl.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
queueEl.innerHTML = this.craftingQueue.map(item => {
|
||||||
|
const recipe = this.recipes[item.recipeId];
|
||||||
|
const remaining = Math.max(0, item.endTime - now);
|
||||||
|
const seconds = Math.ceil(remaining / 1000);
|
||||||
|
const progress = ((now - item.startTime) / (item.endTime - item.startTime)) * 100;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="queue-item">
|
||||||
|
<span class="queue-icon">${recipe.icon}</span>
|
||||||
|
<span class="queue-name">${recipe.name}</span>
|
||||||
|
<div class="queue-progress">
|
||||||
|
<div class="queue-progress-fill" style="width: ${progress}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="queue-time">${seconds}s</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRecipes(category) {
|
||||||
|
const recipesEl = document.getElementById('crafting-recipes');
|
||||||
|
if (!recipesEl) return;
|
||||||
|
|
||||||
|
const categoryRecipes = Object.values(this.recipes).filter(r => r.category === category);
|
||||||
|
|
||||||
|
recipesEl.innerHTML = categoryRecipes.map(recipe => {
|
||||||
|
const canCraft = this.canCraft(recipe.id);
|
||||||
|
const count = this.craftedItems[recipe.id] || 0;
|
||||||
|
const inventory = this.gameEngine.getInventorySummary();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="recipe-card ${canCraft ? 'craftable' : 'locked'}" data-recipe="${recipe.id}">
|
||||||
|
<div class="recipe-icon">${recipe.icon}</div>
|
||||||
|
<div class="recipe-info">
|
||||||
|
<div class="recipe-name">${recipe.name}</div>
|
||||||
|
<div class="recipe-desc">${recipe.description}</div>
|
||||||
|
<div class="recipe-ingredients">
|
||||||
|
${Object.entries(recipe.ingredients).map(([item, required]) => {
|
||||||
|
const have = inventory[item] || 0;
|
||||||
|
const enough = have >= required;
|
||||||
|
return `<span class="${enough ? 'enough' : 'need'}">${this.getItemIcon(item)}${have}/${required}</span>`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="recipe-action">
|
||||||
|
${count > 0 ? `<span class="recipe-count">x${count}</span>` : ''}
|
||||||
|
<button class="craft-btn" ${!canCraft ? 'disabled' : ''}>Craft</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Add click handlers
|
||||||
|
recipesEl.querySelectorAll('.craft-btn:not([disabled])').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const recipeId = e.target.closest('.recipe-card').dataset.recipe;
|
||||||
|
this.craft(recipeId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEquipped() {
|
||||||
|
const equippedEl = document.getElementById('equipped-items');
|
||||||
|
if (!equippedEl) return;
|
||||||
|
|
||||||
|
if (this.equippedItems.length === 0) {
|
||||||
|
equippedEl.innerHTML = '<div class="no-equipped">No equipment active</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
equippedEl.innerHTML = `
|
||||||
|
<div class="equipped-header">Equipped (${this.equippedItems.length}/3)</div>
|
||||||
|
<div class="equipped-grid">
|
||||||
|
${this.equippedItems.map(itemId => {
|
||||||
|
const recipe = this.recipes[itemId];
|
||||||
|
return `
|
||||||
|
<div class="equipped-slot" data-item="${itemId}">
|
||||||
|
<span class="equipped-icon">${recipe.icon}</span>
|
||||||
|
<span class="equipped-name">${recipe.name}</span>
|
||||||
|
<button class="unequip-btn">✕</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
equippedEl.querySelectorAll('.unequip-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const itemId = e.target.closest('.equipped-slot').dataset.item;
|
||||||
|
this.equipItem(itemId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemIcon(type) {
|
||||||
|
const icons = { stars: '⭐', gems: '💎', trophies: '🏆', keys: '🗝️' };
|
||||||
|
return icons[type] || '✨';
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isVisible = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
src/js/dayNightCycle.js
Normal file
303
src/js/dayNightCycle.js
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
// Day/Night Cycle System for RoadWorld
|
||||||
|
// Creates dynamic time-of-day effects with special events
|
||||||
|
|
||||||
|
export class DayNightCycle {
|
||||||
|
constructor(mapManager, gameEngine, soundManager) {
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.soundManager = soundManager;
|
||||||
|
|
||||||
|
// Time settings (accelerated game time)
|
||||||
|
this.realSecondsPerGameHour = 60; // 1 real minute = 1 game hour
|
||||||
|
this.gameTime = 12 * 60; // Start at noon (minutes since midnight)
|
||||||
|
this.isRunning = false;
|
||||||
|
this.updateInterval = null;
|
||||||
|
|
||||||
|
// Time periods
|
||||||
|
this.periods = {
|
||||||
|
dawn: { start: 5 * 60, end: 7 * 60, name: 'Dawn', icon: '🌅' },
|
||||||
|
morning: { start: 7 * 60, end: 12 * 60, name: 'Morning', icon: '☀️' },
|
||||||
|
afternoon: { start: 12 * 60, end: 17 * 60, name: 'Afternoon', icon: '🌤️' },
|
||||||
|
dusk: { start: 17 * 60, end: 19 * 60, name: 'Dusk', icon: '🌇' },
|
||||||
|
evening: { start: 19 * 60, end: 22 * 60, name: 'Evening', icon: '🌆' },
|
||||||
|
night: { start: 22 * 60, end: 5 * 60, name: 'Night', icon: '🌙' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Time-based bonuses
|
||||||
|
this.bonuses = {
|
||||||
|
dawn: { xpMultiplier: 1.5, description: 'Early Bird Bonus' },
|
||||||
|
night: { xpMultiplier: 1.3, description: 'Night Owl Bonus' },
|
||||||
|
goldenHour: { xpMultiplier: 2.0, description: 'Golden Hour!' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Special events
|
||||||
|
this.activeEvents = [];
|
||||||
|
this.eventCooldowns = {};
|
||||||
|
|
||||||
|
this.displayElement = null;
|
||||||
|
this.overlayElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createDisplay();
|
||||||
|
this.createOverlay();
|
||||||
|
this.syncWithRealTime();
|
||||||
|
this.start();
|
||||||
|
|
||||||
|
console.log('🌅 Day/Night Cycle initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createDisplay() {
|
||||||
|
this.displayElement = document.createElement('div');
|
||||||
|
this.displayElement.className = 'time-display';
|
||||||
|
this.displayElement.innerHTML = `
|
||||||
|
<div class="time-icon" id="time-icon">☀️</div>
|
||||||
|
<div class="time-info">
|
||||||
|
<div class="time-clock" id="time-clock">12:00</div>
|
||||||
|
<div class="time-period" id="time-period">Afternoon</div>
|
||||||
|
</div>
|
||||||
|
<div class="time-bonus" id="time-bonus" style="display: none;"></div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this.displayElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
createOverlay() {
|
||||||
|
this.overlayElement = document.createElement('div');
|
||||||
|
this.overlayElement.className = 'day-night-overlay';
|
||||||
|
this.overlayElement.id = 'day-night-overlay';
|
||||||
|
document.body.appendChild(this.overlayElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncWithRealTime() {
|
||||||
|
// Option to sync with real-world time
|
||||||
|
const now = new Date();
|
||||||
|
this.gameTime = now.getHours() * 60 + now.getMinutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
// Update every second (real time)
|
||||||
|
this.updateInterval = setInterval(() => {
|
||||||
|
this.tick();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
this.updateDisplay();
|
||||||
|
this.updateOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.isRunning = false;
|
||||||
|
if (this.updateInterval) {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
this.updateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
// Advance game time
|
||||||
|
const minutesPerSecond = 60 / this.realSecondsPerGameHour;
|
||||||
|
this.gameTime += minutesPerSecond;
|
||||||
|
|
||||||
|
// Wrap around midnight
|
||||||
|
if (this.gameTime >= 24 * 60) {
|
||||||
|
this.gameTime -= 24 * 60;
|
||||||
|
this.onNewDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDisplay();
|
||||||
|
this.updateOverlay();
|
||||||
|
this.checkTimeEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewDay() {
|
||||||
|
// Trigger new day events
|
||||||
|
if (this.gameEngine) {
|
||||||
|
this.showNotification('🌅 A new day begins!', 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPeriod() {
|
||||||
|
const time = this.gameTime;
|
||||||
|
|
||||||
|
for (const [key, period] of Object.entries(this.periods)) {
|
||||||
|
if (key === 'night') {
|
||||||
|
// Night wraps around midnight
|
||||||
|
if (time >= period.start || time < period.end) {
|
||||||
|
return { key, ...period };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (time >= period.start && time < period.end) {
|
||||||
|
return { key, ...period };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key: 'day', name: 'Day', icon: '☀️' };
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormattedTime() {
|
||||||
|
const hours = Math.floor(this.gameTime / 60);
|
||||||
|
const minutes = Math.floor(this.gameTime % 60);
|
||||||
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
const period = this.getCurrentPeriod();
|
||||||
|
const timeStr = this.getFormattedTime();
|
||||||
|
|
||||||
|
document.getElementById('time-icon').textContent = period.icon;
|
||||||
|
document.getElementById('time-clock').textContent = timeStr;
|
||||||
|
document.getElementById('time-period').textContent = period.name;
|
||||||
|
|
||||||
|
// Show active bonus
|
||||||
|
const bonus = this.getCurrentBonus();
|
||||||
|
const bonusEl = document.getElementById('time-bonus');
|
||||||
|
if (bonus) {
|
||||||
|
bonusEl.style.display = 'block';
|
||||||
|
bonusEl.textContent = `${bonus.description} x${bonus.xpMultiplier}`;
|
||||||
|
} else {
|
||||||
|
bonusEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOverlay() {
|
||||||
|
const period = this.getCurrentPeriod();
|
||||||
|
const overlay = this.overlayElement;
|
||||||
|
|
||||||
|
// Calculate overlay opacity and color based on time
|
||||||
|
let opacity = 0;
|
||||||
|
let color = 'rgba(0, 0, 0, 0)';
|
||||||
|
|
||||||
|
switch (period.key) {
|
||||||
|
case 'dawn':
|
||||||
|
opacity = 0.1;
|
||||||
|
color = `rgba(255, 180, 100, ${opacity})`;
|
||||||
|
break;
|
||||||
|
case 'morning':
|
||||||
|
opacity = 0;
|
||||||
|
break;
|
||||||
|
case 'afternoon':
|
||||||
|
opacity = 0.05;
|
||||||
|
color = `rgba(255, 200, 150, ${opacity})`;
|
||||||
|
break;
|
||||||
|
case 'dusk':
|
||||||
|
opacity = 0.15;
|
||||||
|
color = `rgba(255, 100, 50, ${opacity})`;
|
||||||
|
break;
|
||||||
|
case 'evening':
|
||||||
|
opacity = 0.25;
|
||||||
|
color = `rgba(50, 50, 100, ${opacity})`;
|
||||||
|
break;
|
||||||
|
case 'night':
|
||||||
|
opacity = 0.4;
|
||||||
|
color = `rgba(10, 10, 40, ${opacity})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.style.background = color;
|
||||||
|
overlay.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentBonus() {
|
||||||
|
const period = this.getCurrentPeriod();
|
||||||
|
|
||||||
|
// Check for golden hour (just after dawn or before dusk)
|
||||||
|
const time = this.gameTime;
|
||||||
|
if ((time >= 6 * 60 && time < 7 * 60) || (time >= 18 * 60 && time < 19 * 60)) {
|
||||||
|
return this.bonuses.goldenHour;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.bonuses[period.key]) {
|
||||||
|
return this.bonuses[period.key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getXPMultiplier() {
|
||||||
|
const bonus = this.getCurrentBonus();
|
||||||
|
return bonus ? bonus.xpMultiplier : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTimeEvents() {
|
||||||
|
const period = this.getCurrentPeriod();
|
||||||
|
const time = this.gameTime;
|
||||||
|
|
||||||
|
// Midnight event
|
||||||
|
if (time >= 0 && time < 1 && !this.eventCooldowns.midnight) {
|
||||||
|
this.triggerEvent('midnight', {
|
||||||
|
name: 'Midnight Mystery',
|
||||||
|
description: 'Rare items spawn more frequently!',
|
||||||
|
icon: '🌑',
|
||||||
|
duration: 60 // 1 game hour
|
||||||
|
});
|
||||||
|
this.eventCooldowns.midnight = true;
|
||||||
|
setTimeout(() => { this.eventCooldowns.midnight = false; }, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noon event
|
||||||
|
if (time >= 12 * 60 && time < 12 * 60 + 1 && !this.eventCooldowns.noon) {
|
||||||
|
this.triggerEvent('noon', {
|
||||||
|
name: 'High Noon',
|
||||||
|
description: 'Double XP for movement!',
|
||||||
|
icon: '🌞',
|
||||||
|
duration: 30
|
||||||
|
});
|
||||||
|
this.eventCooldowns.noon = true;
|
||||||
|
setTimeout(() => { this.eventCooldowns.noon = false; }, 60000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvent(type, eventData) {
|
||||||
|
this.activeEvents.push({
|
||||||
|
type,
|
||||||
|
...eventData,
|
||||||
|
startTime: this.gameTime,
|
||||||
|
endTime: this.gameTime + eventData.duration
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showNotification(`${eventData.icon} ${eventData.name}: ${eventData.description}`, 'event');
|
||||||
|
|
||||||
|
if (this.soundManager) {
|
||||||
|
this.soundManager.playAchievement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEventActive(type) {
|
||||||
|
return this.activeEvents.some(e => e.type === type && this.gameTime < e.endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message, type = 'info') {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `time-notification ${type}`;
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('fade-out');
|
||||||
|
setTimeout(() => notification.remove(), 500);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeScale(scale) {
|
||||||
|
// Adjust how fast time passes (1 = normal, 2 = double speed)
|
||||||
|
this.realSecondsPerGameHour = 60 / scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTime(hours, minutes = 0) {
|
||||||
|
this.gameTime = hours * 60 + minutes;
|
||||||
|
this.updateDisplay();
|
||||||
|
this.updateOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
isNight() {
|
||||||
|
const period = this.getCurrentPeriod();
|
||||||
|
return period.key === 'night' || period.key === 'evening';
|
||||||
|
}
|
||||||
|
|
||||||
|
isDawn() {
|
||||||
|
return this.getCurrentPeriod().key === 'dawn';
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/js/main.js
133
src/js/main.js
@@ -29,6 +29,16 @@ import { ProfileManager } from './profileManager.js';
|
|||||||
import { ParticleEffects } from './particleEffects.js';
|
import { ParticleEffects } from './particleEffects.js';
|
||||||
import { QuickTravel } from './quickTravel.js';
|
import { QuickTravel } from './quickTravel.js';
|
||||||
|
|
||||||
|
// Ultimate features (v6.0)
|
||||||
|
import { DayNightCycle } from './dayNightCycle.js';
|
||||||
|
import { WorldEvents } from './worldEvents.js';
|
||||||
|
import { PetCompanion } from './petCompanion.js';
|
||||||
|
import { TreasureHunt } from './treasureHunt.js';
|
||||||
|
import { SkillTree } from './skillTree.js';
|
||||||
|
import { CraftingSystem } from './craftingSystem.js';
|
||||||
|
import { ChallengeModes } from './challengeModes.js';
|
||||||
|
import { PhotoMode } from './photoMode.js';
|
||||||
|
|
||||||
class RoadWorldApp {
|
class RoadWorldApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.mapManager = null;
|
this.mapManager = null;
|
||||||
@@ -64,6 +74,16 @@ class RoadWorldApp {
|
|||||||
this.profileManager = null;
|
this.profileManager = null;
|
||||||
this.particleEffects = null;
|
this.particleEffects = null;
|
||||||
this.quickTravel = null;
|
this.quickTravel = null;
|
||||||
|
|
||||||
|
// Ultimate features (v6.0)
|
||||||
|
this.dayNightCycle = null;
|
||||||
|
this.worldEvents = null;
|
||||||
|
this.petCompanion = null;
|
||||||
|
this.treasureHunt = null;
|
||||||
|
this.skillTree = null;
|
||||||
|
this.craftingSystem = null;
|
||||||
|
this.challengeModes = null;
|
||||||
|
this.photoMode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -124,10 +144,13 @@ class RoadWorldApp {
|
|||||||
// Initialize next-level features (v5.0)
|
// Initialize next-level features (v5.0)
|
||||||
this.initNextLevelFeatures();
|
this.initNextLevelFeatures();
|
||||||
|
|
||||||
|
// Initialize ultimate features (v6.0)
|
||||||
|
this.initUltimateFeatures();
|
||||||
|
|
||||||
// Make app globally accessible
|
// Make app globally accessible
|
||||||
window.app = this;
|
window.app = this;
|
||||||
|
|
||||||
console.log('RoadWorld v5.0 initialized - NEXT LEVEL!');
|
console.log('RoadWorld v6.0 initialized - ULTIMATE EDITION!');
|
||||||
}
|
}
|
||||||
|
|
||||||
initEnhancedFeatures() {
|
initEnhancedFeatures() {
|
||||||
@@ -239,6 +262,114 @@ class RoadWorldApp {
|
|||||||
this.checkDailyLogin();
|
this.checkDailyLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initUltimateFeatures() {
|
||||||
|
// Day/Night Cycle
|
||||||
|
this.dayNightCycle = new DayNightCycle(this.mapManager, this.gameEngine, this.soundManager);
|
||||||
|
this.dayNightCycle.init();
|
||||||
|
|
||||||
|
// World Events
|
||||||
|
this.worldEvents = new WorldEvents(this.gameEngine, this.mapManager, this.storageManager);
|
||||||
|
this.worldEvents.init();
|
||||||
|
|
||||||
|
// Pet Companion
|
||||||
|
this.petCompanion = new PetCompanion(this.mapManager, this.gameEngine, this.storageManager);
|
||||||
|
this.petCompanion.init();
|
||||||
|
|
||||||
|
// Treasure Hunt
|
||||||
|
this.treasureHunt = new TreasureHunt(this.mapManager, this.gameEngine, this.storageManager);
|
||||||
|
this.treasureHunt.init();
|
||||||
|
|
||||||
|
// Skill Tree
|
||||||
|
this.skillTree = new SkillTree(this.gameEngine, this.storageManager);
|
||||||
|
this.skillTree.init();
|
||||||
|
|
||||||
|
// Crafting System
|
||||||
|
this.craftingSystem = new CraftingSystem(this.gameEngine, this.storageManager);
|
||||||
|
this.craftingSystem.init();
|
||||||
|
|
||||||
|
// Challenge Modes
|
||||||
|
this.challengeModes = new ChallengeModes(this.mapManager, this.gameEngine, this.storageManager);
|
||||||
|
this.challengeModes.init();
|
||||||
|
|
||||||
|
// Photo Mode
|
||||||
|
this.photoMode = new PhotoMode(this.mapManager, this.storageManager);
|
||||||
|
this.photoMode.init();
|
||||||
|
|
||||||
|
// Setup v6.0 controls
|
||||||
|
this.setupUltimateControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupUltimateControls() {
|
||||||
|
// Day/Night toggle
|
||||||
|
const dayNightBtn = document.getElementById('btn-daynight');
|
||||||
|
if (dayNightBtn) {
|
||||||
|
dayNightBtn.addEventListener('click', () => {
|
||||||
|
// Cycle through time periods
|
||||||
|
const times = [6, 12, 18, 0]; // Dawn, Noon, Dusk, Midnight
|
||||||
|
const currentHour = Math.floor(this.dayNightCycle.gameTime / 60);
|
||||||
|
const nextIndex = times.findIndex(t => t > currentHour) || 0;
|
||||||
|
this.dayNightCycle.setTime(times[nextIndex]);
|
||||||
|
this.showNotification(`Time set to ${times[nextIndex]}:00`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// World Events button
|
||||||
|
const eventsBtn = document.getElementById('btn-events');
|
||||||
|
if (eventsBtn) {
|
||||||
|
eventsBtn.addEventListener('click', () => {
|
||||||
|
this.worldEvents.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pet button
|
||||||
|
const petBtn = document.getElementById('btn-pet');
|
||||||
|
if (petBtn) {
|
||||||
|
petBtn.addEventListener('click', () => {
|
||||||
|
this.petCompanion.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treasure Hunt button
|
||||||
|
const huntBtn = document.getElementById('btn-hunt');
|
||||||
|
if (huntBtn) {
|
||||||
|
huntBtn.addEventListener('click', () => {
|
||||||
|
this.treasureHunt.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skills button
|
||||||
|
const skillsBtn = document.getElementById('btn-skills');
|
||||||
|
if (skillsBtn) {
|
||||||
|
skillsBtn.addEventListener('click', () => {
|
||||||
|
this.skillTree.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crafting button
|
||||||
|
const craftBtn = document.getElementById('btn-craft');
|
||||||
|
if (craftBtn) {
|
||||||
|
craftBtn.addEventListener('click', () => {
|
||||||
|
this.craftingSystem.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Challenges button
|
||||||
|
const challengeBtn = document.getElementById('btn-challenge');
|
||||||
|
if (challengeBtn) {
|
||||||
|
challengeBtn.addEventListener('click', () => {
|
||||||
|
this.challengeModes.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photo Mode button
|
||||||
|
const photoBtn = document.getElementById('btn-photo');
|
||||||
|
if (photoBtn) {
|
||||||
|
photoBtn.addEventListener('click', () => {
|
||||||
|
this.photoMode.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupNextLevelControls() {
|
setupNextLevelControls() {
|
||||||
// Screenshot button
|
// Screenshot button
|
||||||
const screenshotBtn = document.getElementById('btn-screenshot');
|
const screenshotBtn = document.getElementById('btn-screenshot');
|
||||||
|
|||||||
498
src/js/petCompanion.js
Normal file
498
src/js/petCompanion.js
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
// Pet Companion System for RoadWorld
|
||||||
|
// Cute companions that follow you and provide bonuses
|
||||||
|
|
||||||
|
export class PetCompanion {
|
||||||
|
constructor(mapManager, gameEngine, storageManager) {
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
this.activePet = null;
|
||||||
|
this.petMarker = null;
|
||||||
|
this.panelElement = null;
|
||||||
|
this.isVisible = false;
|
||||||
|
|
||||||
|
// Pet definitions
|
||||||
|
this.pets = {
|
||||||
|
cat: {
|
||||||
|
id: 'cat',
|
||||||
|
name: 'Luna',
|
||||||
|
icon: '🐱',
|
||||||
|
rarity: 'common',
|
||||||
|
description: 'A curious cat that loves exploring!',
|
||||||
|
bonus: { type: 'xp', value: 1.1, description: '+10% XP' },
|
||||||
|
unlocked: true,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 100
|
||||||
|
},
|
||||||
|
dog: {
|
||||||
|
id: 'dog',
|
||||||
|
name: 'Max',
|
||||||
|
icon: '🐕',
|
||||||
|
rarity: 'common',
|
||||||
|
description: 'A loyal companion that finds items!',
|
||||||
|
bonus: { type: 'itemFind', value: 1.15, description: '+15% Item Find' },
|
||||||
|
unlocked: true,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 100
|
||||||
|
},
|
||||||
|
fox: {
|
||||||
|
id: 'fox',
|
||||||
|
name: 'Ember',
|
||||||
|
icon: '🦊',
|
||||||
|
rarity: 'rare',
|
||||||
|
description: 'A swift fox that grants speed!',
|
||||||
|
bonus: { type: 'speed', value: 1.2, description: '+20% Speed' },
|
||||||
|
unlocked: false,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 150
|
||||||
|
},
|
||||||
|
owl: {
|
||||||
|
id: 'owl',
|
||||||
|
name: 'Wisdom',
|
||||||
|
icon: '🦉',
|
||||||
|
rarity: 'rare',
|
||||||
|
description: 'A wise owl that boosts night bonuses!',
|
||||||
|
bonus: { type: 'nightXP', value: 1.5, description: '+50% Night XP' },
|
||||||
|
unlocked: false,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 150
|
||||||
|
},
|
||||||
|
dragon: {
|
||||||
|
id: 'dragon',
|
||||||
|
name: 'Spark',
|
||||||
|
icon: '🐉',
|
||||||
|
rarity: 'epic',
|
||||||
|
description: 'A baby dragon with fiery power!',
|
||||||
|
bonus: { type: 'xp', value: 1.25, description: '+25% XP' },
|
||||||
|
unlocked: false,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 250
|
||||||
|
},
|
||||||
|
phoenix: {
|
||||||
|
id: 'phoenix',
|
||||||
|
name: 'Blaze',
|
||||||
|
icon: '🔥',
|
||||||
|
rarity: 'legendary',
|
||||||
|
description: 'A legendary phoenix that doubles streak bonuses!',
|
||||||
|
bonus: { type: 'streak', value: 2.0, description: '2x Streak Bonus' },
|
||||||
|
unlocked: false,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 500
|
||||||
|
},
|
||||||
|
unicorn: {
|
||||||
|
id: 'unicorn',
|
||||||
|
name: 'Starlight',
|
||||||
|
icon: '🦄',
|
||||||
|
rarity: 'legendary',
|
||||||
|
description: 'A magical unicorn with mystical powers!',
|
||||||
|
bonus: { type: 'allStats', value: 1.15, description: '+15% All Stats' },
|
||||||
|
unlocked: false,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 500
|
||||||
|
},
|
||||||
|
robot: {
|
||||||
|
id: 'robot',
|
||||||
|
name: 'Byte',
|
||||||
|
icon: '🤖',
|
||||||
|
rarity: 'epic',
|
||||||
|
description: 'A robot companion that auto-collects nearby items!',
|
||||||
|
bonus: { type: 'autoCollect', value: 50, description: 'Auto-collect 50m' },
|
||||||
|
unlocked: false,
|
||||||
|
level: 1,
|
||||||
|
xp: 0,
|
||||||
|
xpToLevel: 250
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
this.animationFrame = 0;
|
||||||
|
this.animationInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.loadPets();
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
console.log('🐾 Pet Companion System initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'pet-panel ui-overlay';
|
||||||
|
this.panelElement.id = 'pet-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>🐾 Pet Companions</span>
|
||||||
|
<button class="panel-close" id="pet-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content pet-content">
|
||||||
|
<div class="active-pet-section" id="active-pet-section">
|
||||||
|
<h4>Active Companion</h4>
|
||||||
|
<div class="active-pet-display" id="active-pet-display"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pet-collection-section">
|
||||||
|
<h4>Collection</h4>
|
||||||
|
<div class="pet-grid" id="pet-grid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('pet-close').addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPets() {
|
||||||
|
const saved = this.storageManager.data.pets || {};
|
||||||
|
|
||||||
|
// Load unlocked status and levels
|
||||||
|
Object.keys(saved).forEach(petId => {
|
||||||
|
if (this.pets[petId]) {
|
||||||
|
this.pets[petId] = { ...this.pets[petId], ...saved[petId] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load active pet
|
||||||
|
if (saved.active && this.pets[saved.active]?.unlocked) {
|
||||||
|
this.equipPet(saved.active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
savePets() {
|
||||||
|
const petData = {};
|
||||||
|
Object.entries(this.pets).forEach(([id, pet]) => {
|
||||||
|
petData[id] = {
|
||||||
|
unlocked: pet.unlocked,
|
||||||
|
level: pet.level,
|
||||||
|
xp: pet.xp,
|
||||||
|
name: pet.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
petData.active = this.activePet?.id || null;
|
||||||
|
|
||||||
|
this.storageManager.data.pets = petData;
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockPet(petId) {
|
||||||
|
if (this.pets[petId]) {
|
||||||
|
this.pets[petId].unlocked = true;
|
||||||
|
this.savePets();
|
||||||
|
this.showNotification(`🎉 New pet unlocked: ${this.pets[petId].icon} ${this.pets[petId].name}!`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
equipPet(petId) {
|
||||||
|
const pet = this.pets[petId];
|
||||||
|
if (!pet || !pet.unlocked) return false;
|
||||||
|
|
||||||
|
// Remove current pet marker
|
||||||
|
if (this.petMarker) {
|
||||||
|
this.petMarker.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activePet = pet;
|
||||||
|
|
||||||
|
// Create pet marker on map
|
||||||
|
this.createPetMarker();
|
||||||
|
|
||||||
|
// Start animations
|
||||||
|
this.startAnimation();
|
||||||
|
|
||||||
|
this.savePets();
|
||||||
|
this.renderPanel();
|
||||||
|
|
||||||
|
this.showNotification(`${pet.icon} ${pet.name} is now following you!`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
unequipPet() {
|
||||||
|
if (this.petMarker) {
|
||||||
|
this.petMarker.remove();
|
||||||
|
this.petMarker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopAnimation();
|
||||||
|
this.activePet = null;
|
||||||
|
this.savePets();
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
createPetMarker() {
|
||||||
|
if (!this.activePet || !this.mapManager) return;
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `pet-marker ${this.activePet.rarity}`;
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="pet-icon">${this.activePet.icon}</div>
|
||||||
|
<div class="pet-level">Lv.${this.activePet.level}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const center = this.mapManager.getCenter();
|
||||||
|
const offset = this.getRandomOffset();
|
||||||
|
|
||||||
|
this.petMarker = new maplibregl.Marker({ element: el, anchor: 'center' })
|
||||||
|
.setLngLat([center.lng + offset.lng, center.lat + offset.lat])
|
||||||
|
.addTo(this.mapManager.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomOffset() {
|
||||||
|
// Random position near player
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const distance = 0.0002 + Math.random() * 0.0003;
|
||||||
|
return {
|
||||||
|
lng: Math.cos(angle) * distance,
|
||||||
|
lat: Math.sin(angle) * distance
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimation() {
|
||||||
|
this.animationInterval = setInterval(() => {
|
||||||
|
this.animatePet();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAnimation() {
|
||||||
|
if (this.animationInterval) {
|
||||||
|
clearInterval(this.animationInterval);
|
||||||
|
this.animationInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animatePet() {
|
||||||
|
if (!this.petMarker || !this.activePet) return;
|
||||||
|
|
||||||
|
// Hop animation
|
||||||
|
this.animationFrame = (this.animationFrame + 1) % 4;
|
||||||
|
const el = this.petMarker.getElement();
|
||||||
|
if (el) {
|
||||||
|
el.style.transform = `translateY(${this.animationFrame === 1 ? -5 : 0}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow player position with lag
|
||||||
|
const center = this.mapManager.getCenter();
|
||||||
|
const currentPos = this.petMarker.getLngLat();
|
||||||
|
|
||||||
|
const newLng = currentPos.lng + (center.lng - currentPos.lng) * 0.1;
|
||||||
|
const newLat = currentPos.lat + (center.lat - currentPos.lat) * 0.1;
|
||||||
|
|
||||||
|
// Add some wander
|
||||||
|
const wander = 0.00005;
|
||||||
|
this.petMarker.setLngLat([
|
||||||
|
newLng + (Math.random() - 0.5) * wander,
|
||||||
|
newLat + (Math.random() - 0.5) * wander
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
addPetXP(amount) {
|
||||||
|
if (!this.activePet) return;
|
||||||
|
|
||||||
|
this.activePet.xp += amount;
|
||||||
|
|
||||||
|
// Check level up
|
||||||
|
while (this.activePet.xp >= this.activePet.xpToLevel) {
|
||||||
|
this.activePet.xp -= this.activePet.xpToLevel;
|
||||||
|
this.levelUpPet();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savePets();
|
||||||
|
}
|
||||||
|
|
||||||
|
levelUpPet() {
|
||||||
|
if (!this.activePet) return;
|
||||||
|
|
||||||
|
this.activePet.level++;
|
||||||
|
this.activePet.xpToLevel = Math.floor(this.activePet.xpToLevel * 1.5);
|
||||||
|
|
||||||
|
// Improve bonus
|
||||||
|
if (typeof this.activePet.bonus.value === 'number') {
|
||||||
|
this.activePet.bonus.value *= 1.05; // 5% improvement per level
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showNotification(`🎉 ${this.activePet.icon} ${this.activePet.name} leveled up to ${this.activePet.level}!`);
|
||||||
|
|
||||||
|
// Update marker
|
||||||
|
if (this.petMarker) {
|
||||||
|
const levelEl = this.petMarker.getElement().querySelector('.pet-level');
|
||||||
|
if (levelEl) {
|
||||||
|
levelEl.textContent = `Lv.${this.activePet.level}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBonusMultiplier(type) {
|
||||||
|
if (!this.activePet) return 1;
|
||||||
|
|
||||||
|
const bonus = this.activePet.bonus;
|
||||||
|
|
||||||
|
if (bonus.type === type) {
|
||||||
|
return bonus.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bonus.type === 'allStats') {
|
||||||
|
return bonus.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAutoCollectRange() {
|
||||||
|
if (!this.activePet || this.activePet.bonus.type !== 'autoCollect') return 0;
|
||||||
|
return this.activePet.bonus.value * (1 + (this.activePet.level - 1) * 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
renamePet(petId, newName) {
|
||||||
|
if (this.pets[petId]) {
|
||||||
|
this.pets[petId].name = newName;
|
||||||
|
this.savePets();
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPanel() {
|
||||||
|
// Active pet display
|
||||||
|
const activeDisplay = document.getElementById('active-pet-display');
|
||||||
|
if (activeDisplay) {
|
||||||
|
if (this.activePet) {
|
||||||
|
const xpPercent = (this.activePet.xp / this.activePet.xpToLevel) * 100;
|
||||||
|
activeDisplay.innerHTML = `
|
||||||
|
<div class="active-pet-card ${this.activePet.rarity}">
|
||||||
|
<div class="active-pet-icon">${this.activePet.icon}</div>
|
||||||
|
<div class="active-pet-info">
|
||||||
|
<div class="active-pet-name">${this.activePet.name}</div>
|
||||||
|
<div class="active-pet-level">Level ${this.activePet.level}</div>
|
||||||
|
<div class="active-pet-xp-bar">
|
||||||
|
<div class="active-pet-xp-fill" style="width: ${xpPercent}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="active-pet-bonus">${this.activePet.bonus.description}</div>
|
||||||
|
</div>
|
||||||
|
<button class="pet-unequip-btn" id="unequip-pet">Remove</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('unequip-pet')?.addEventListener('click', () => {
|
||||||
|
this.unequipPet();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activeDisplay.innerHTML = `
|
||||||
|
<div class="no-active-pet">No pet active. Select one below!</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pet grid
|
||||||
|
const petGrid = document.getElementById('pet-grid');
|
||||||
|
if (petGrid) {
|
||||||
|
petGrid.innerHTML = Object.entries(this.pets).map(([id, pet]) => `
|
||||||
|
<div class="pet-card ${pet.rarity} ${pet.unlocked ? '' : 'locked'} ${this.activePet?.id === id ? 'equipped' : ''}"
|
||||||
|
data-pet="${id}">
|
||||||
|
<div class="pet-card-icon">${pet.unlocked ? pet.icon : '❓'}</div>
|
||||||
|
<div class="pet-card-name">${pet.unlocked ? pet.name : '???'}</div>
|
||||||
|
<div class="pet-card-rarity">${pet.rarity}</div>
|
||||||
|
${pet.unlocked ? `
|
||||||
|
<div class="pet-card-level">Lv.${pet.level}</div>
|
||||||
|
<div class="pet-card-bonus">${pet.bonus.description}</div>
|
||||||
|
` : `
|
||||||
|
<div class="pet-card-locked">Locked</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Add click handlers
|
||||||
|
petGrid.querySelectorAll('.pet-card:not(.locked)').forEach(card => {
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
const petId = card.dataset.pet;
|
||||||
|
if (this.activePet?.id !== petId) {
|
||||||
|
this.equipPet(petId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unlocks based on achievements
|
||||||
|
checkUnlocks() {
|
||||||
|
const player = this.gameEngine?.player;
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
// Fox unlocked at level 10
|
||||||
|
if (player.level >= 10 && !this.pets.fox.unlocked) {
|
||||||
|
this.unlockPet('fox');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owl unlocked after 50km traveled
|
||||||
|
if (player.stats.distanceTraveled >= 50000 && !this.pets.owl.unlocked) {
|
||||||
|
this.unlockPet('owl');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragon unlocked after 500 items
|
||||||
|
if (player.stats.itemsCollected >= 500 && !this.pets.dragon.unlocked) {
|
||||||
|
this.unlockPet('dragon');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robot unlocked at level 25
|
||||||
|
if (player.level >= 25 && !this.pets.robot.unlocked) {
|
||||||
|
this.unlockPet('robot');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phoenix unlocked after 30 day streak
|
||||||
|
if (player.stats.longestStreak >= 30 && !this.pets.phoenix.unlocked) {
|
||||||
|
this.unlockPet('phoenix');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicorn unlocked at level 50
|
||||||
|
if (player.level >= 50 && !this.pets.unicorn.unlocked) {
|
||||||
|
this.unlockPet('unicorn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isVisible = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
596
src/js/photoMode.js
Normal file
596
src/js/photoMode.js
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
// Photo Mode for RoadWorld
|
||||||
|
// Advanced screenshot system with filters and frames
|
||||||
|
|
||||||
|
export class PhotoMode {
|
||||||
|
constructor(mapManager, storageManager) {
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
this.isActive = false;
|
||||||
|
this.panelElement = null;
|
||||||
|
this.previewElement = null;
|
||||||
|
this.currentPhoto = null;
|
||||||
|
|
||||||
|
// Filter presets
|
||||||
|
this.filters = {
|
||||||
|
none: {
|
||||||
|
name: 'Original',
|
||||||
|
icon: '📷',
|
||||||
|
css: 'none'
|
||||||
|
},
|
||||||
|
vintage: {
|
||||||
|
name: 'Vintage',
|
||||||
|
icon: '📜',
|
||||||
|
css: 'sepia(0.4) contrast(1.1) brightness(0.95)'
|
||||||
|
},
|
||||||
|
noir: {
|
||||||
|
name: 'Noir',
|
||||||
|
icon: '🖤',
|
||||||
|
css: 'grayscale(1) contrast(1.2)'
|
||||||
|
},
|
||||||
|
cyberpunk: {
|
||||||
|
name: 'Cyberpunk',
|
||||||
|
icon: '💜',
|
||||||
|
css: 'saturate(1.5) hue-rotate(30deg) contrast(1.1)'
|
||||||
|
},
|
||||||
|
sunset: {
|
||||||
|
name: 'Sunset',
|
||||||
|
icon: '🌅',
|
||||||
|
css: 'saturate(1.3) sepia(0.2) brightness(1.05) contrast(1.1)'
|
||||||
|
},
|
||||||
|
arctic: {
|
||||||
|
name: 'Arctic',
|
||||||
|
icon: '❄️',
|
||||||
|
css: 'saturate(0.8) brightness(1.1) hue-rotate(-10deg)'
|
||||||
|
},
|
||||||
|
dream: {
|
||||||
|
name: 'Dream',
|
||||||
|
icon: '💭',
|
||||||
|
css: 'saturate(1.2) blur(0.5px) brightness(1.1)'
|
||||||
|
},
|
||||||
|
neon: {
|
||||||
|
name: 'Neon',
|
||||||
|
icon: '🌈',
|
||||||
|
css: 'saturate(2) contrast(1.3)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Frame presets
|
||||||
|
this.frames = {
|
||||||
|
none: {
|
||||||
|
name: 'None',
|
||||||
|
icon: '⬜',
|
||||||
|
template: null
|
||||||
|
},
|
||||||
|
polaroid: {
|
||||||
|
name: 'Polaroid',
|
||||||
|
icon: '🖼️',
|
||||||
|
padding: { top: 20, bottom: 60, left: 20, right: 20 },
|
||||||
|
background: '#fff',
|
||||||
|
shadow: true
|
||||||
|
},
|
||||||
|
filmStrip: {
|
||||||
|
name: 'Film Strip',
|
||||||
|
icon: '🎬',
|
||||||
|
padding: { top: 30, bottom: 30, left: 40, right: 40 },
|
||||||
|
background: '#111',
|
||||||
|
perforations: true
|
||||||
|
},
|
||||||
|
vintage: {
|
||||||
|
name: 'Vintage',
|
||||||
|
icon: '🏛️',
|
||||||
|
padding: { top: 30, bottom: 30, left: 30, right: 30 },
|
||||||
|
background: '#e8dcc8',
|
||||||
|
border: true
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
name: 'Modern',
|
||||||
|
icon: '✨',
|
||||||
|
padding: { top: 15, bottom: 40, left: 15, right: 15 },
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
rounded: true
|
||||||
|
},
|
||||||
|
roadworld: {
|
||||||
|
name: 'RoadWorld',
|
||||||
|
icon: '🌍',
|
||||||
|
padding: { top: 50, bottom: 60, left: 20, right: 20 },
|
||||||
|
background: 'linear-gradient(180deg, #0a0f1e 0%, #1a1f3e 100%)',
|
||||||
|
branded: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectedFilter = 'none';
|
||||||
|
this.selectedFrame = 'none';
|
||||||
|
this.photoGallery = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.loadGallery();
|
||||||
|
|
||||||
|
console.log('📸 Photo Mode initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'photo-mode-panel';
|
||||||
|
this.panelElement.id = 'photo-mode-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="photo-mode-header">
|
||||||
|
<span>📸 Photo Mode</span>
|
||||||
|
<button class="photo-mode-exit" id="photo-exit">Exit</button>
|
||||||
|
</div>
|
||||||
|
<div class="photo-mode-content">
|
||||||
|
<div class="photo-controls">
|
||||||
|
<div class="photo-section">
|
||||||
|
<h4>🎨 Filters</h4>
|
||||||
|
<div class="filter-grid" id="filter-grid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="photo-section">
|
||||||
|
<h4>🖼️ Frames</h4>
|
||||||
|
<div class="frame-grid" id="frame-grid"></div>
|
||||||
|
</div>
|
||||||
|
<div class="photo-section">
|
||||||
|
<h4>📍 Info Overlay</h4>
|
||||||
|
<div class="overlay-options">
|
||||||
|
<label><input type="checkbox" id="show-location" checked> Location</label>
|
||||||
|
<label><input type="checkbox" id="show-coords" checked> Coordinates</label>
|
||||||
|
<label><input type="checkbox" id="show-date" checked> Date</label>
|
||||||
|
<label><input type="checkbox" id="show-watermark" checked> Watermark</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="photo-preview-area">
|
||||||
|
<div class="photo-preview" id="photo-preview"></div>
|
||||||
|
<div class="photo-actions">
|
||||||
|
<button class="photo-btn capture" id="photo-capture">📷 Capture</button>
|
||||||
|
<button class="photo-btn download" id="photo-download" disabled>💾 Save</button>
|
||||||
|
<button class="photo-btn share" id="photo-share" disabled>📤 Share</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="photo-gallery-section">
|
||||||
|
<h4>📚 Gallery (${this.photoGallery.length})</h4>
|
||||||
|
<div class="photo-gallery" id="photo-gallery"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.renderFilters();
|
||||||
|
this.renderFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('photo-exit').addEventListener('click', () => {
|
||||||
|
this.deactivate();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('photo-capture').addEventListener('click', () => {
|
||||||
|
this.capturePhoto();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('photo-download').addEventListener('click', () => {
|
||||||
|
this.downloadPhoto();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('photo-share').addEventListener('click', () => {
|
||||||
|
this.sharePhoto();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFilters() {
|
||||||
|
const grid = document.getElementById('filter-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
grid.innerHTML = Object.entries(this.filters).map(([id, filter]) => `
|
||||||
|
<button class="filter-btn ${this.selectedFilter === id ? 'selected' : ''}"
|
||||||
|
data-filter="${id}">
|
||||||
|
<span class="filter-icon">${filter.icon}</span>
|
||||||
|
<span class="filter-name">${filter.name}</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
grid.querySelectorAll('.filter-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.selectedFilter = btn.dataset.filter;
|
||||||
|
this.renderFilters();
|
||||||
|
this.updatePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFrames() {
|
||||||
|
const grid = document.getElementById('frame-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
grid.innerHTML = Object.entries(this.frames).map(([id, frame]) => `
|
||||||
|
<button class="frame-btn ${this.selectedFrame === id ? 'selected' : ''}"
|
||||||
|
data-frame="${id}">
|
||||||
|
<span class="frame-icon">${frame.icon}</span>
|
||||||
|
<span class="frame-name">${frame.name}</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
grid.querySelectorAll('.frame-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.selectedFrame = btn.dataset.frame;
|
||||||
|
this.renderFrames();
|
||||||
|
this.updatePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
this.isActive = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
|
||||||
|
// Hide other UI elements
|
||||||
|
document.querySelectorAll('.ui-overlay:not(.photo-mode-panel)').forEach(el => {
|
||||||
|
el.style.opacity = '0.3';
|
||||||
|
el.style.pointerEvents = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial capture for preview
|
||||||
|
this.updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this.isActive = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
// Restore UI elements
|
||||||
|
document.querySelectorAll('.ui-overlay').forEach(el => {
|
||||||
|
el.style.opacity = '';
|
||||||
|
el.style.pointerEvents = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isActive) {
|
||||||
|
this.deactivate();
|
||||||
|
} else {
|
||||||
|
this.activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePreview() {
|
||||||
|
const preview = document.getElementById('photo-preview');
|
||||||
|
if (!preview) return;
|
||||||
|
|
||||||
|
preview.innerHTML = '<div class="loading-preview">Generating preview...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canvas = await this.captureMap();
|
||||||
|
const processedCanvas = await this.applyEffects(canvas);
|
||||||
|
|
||||||
|
preview.innerHTML = '';
|
||||||
|
processedCanvas.style.maxWidth = '100%';
|
||||||
|
processedCanvas.style.maxHeight = '300px';
|
||||||
|
preview.appendChild(processedCanvas);
|
||||||
|
} catch (error) {
|
||||||
|
preview.innerHTML = '<div class="preview-error">Preview unavailable</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async capturePhoto() {
|
||||||
|
try {
|
||||||
|
const canvas = await this.captureMap();
|
||||||
|
this.currentPhoto = await this.applyEffects(canvas);
|
||||||
|
|
||||||
|
// Update preview
|
||||||
|
const preview = document.getElementById('photo-preview');
|
||||||
|
preview.innerHTML = '';
|
||||||
|
this.currentPhoto.style.maxWidth = '100%';
|
||||||
|
this.currentPhoto.style.maxHeight = '300px';
|
||||||
|
preview.appendChild(this.currentPhoto.cloneNode(true));
|
||||||
|
|
||||||
|
// Enable buttons
|
||||||
|
document.getElementById('photo-download').disabled = false;
|
||||||
|
document.getElementById('photo-share').disabled = false;
|
||||||
|
|
||||||
|
// Add to gallery
|
||||||
|
this.addToGallery();
|
||||||
|
|
||||||
|
this.showNotification('📸 Photo captured!');
|
||||||
|
} catch (error) {
|
||||||
|
this.showNotification('Failed to capture photo');
|
||||||
|
console.error('Photo capture error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async captureMap() {
|
||||||
|
// Create a canvas from the map
|
||||||
|
const mapCanvas = this.mapManager.map.getCanvas();
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = mapCanvas.width;
|
||||||
|
canvas.height = mapCanvas.height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(mapCanvas, 0, 0);
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyEffects(sourceCanvas) {
|
||||||
|
const filter = this.filters[this.selectedFilter];
|
||||||
|
const frame = this.frames[this.selectedFrame];
|
||||||
|
|
||||||
|
// Calculate final dimensions
|
||||||
|
const padding = frame.padding || { top: 0, bottom: 0, left: 0, right: 0 };
|
||||||
|
const finalWidth = sourceCanvas.width + padding.left + padding.right;
|
||||||
|
const finalHeight = sourceCanvas.height + padding.top + padding.bottom;
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = finalWidth;
|
||||||
|
canvas.height = finalHeight;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Draw frame background
|
||||||
|
if (frame.background) {
|
||||||
|
if (frame.background.startsWith('linear-gradient')) {
|
||||||
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||||
|
gradient.addColorStop(0, '#667eea');
|
||||||
|
gradient.addColorStop(1, '#764ba2');
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = frame.background;
|
||||||
|
}
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filter to source
|
||||||
|
ctx.filter = filter.css || 'none';
|
||||||
|
|
||||||
|
// Draw photo with frame padding
|
||||||
|
ctx.drawImage(
|
||||||
|
sourceCanvas,
|
||||||
|
padding.left,
|
||||||
|
padding.top,
|
||||||
|
sourceCanvas.width,
|
||||||
|
sourceCanvas.height
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.filter = 'none';
|
||||||
|
|
||||||
|
// Add frame decorations
|
||||||
|
if (frame.perforations) {
|
||||||
|
this.drawFilmPerforations(ctx, canvas.width, canvas.height, padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.border) {
|
||||||
|
ctx.strokeStyle = '#8b7355';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeRect(
|
||||||
|
padding.left - 5,
|
||||||
|
padding.top - 5,
|
||||||
|
sourceCanvas.width + 10,
|
||||||
|
sourceCanvas.height + 10
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.shadow) {
|
||||||
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
||||||
|
ctx.shadowBlur = 10;
|
||||||
|
ctx.shadowOffsetX = 3;
|
||||||
|
ctx.shadowOffsetY = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add branded header for RoadWorld frame
|
||||||
|
if (frame.branded) {
|
||||||
|
ctx.fillStyle = '#00d4ff';
|
||||||
|
ctx.font = 'bold 24px Orbitron, sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('ROADWORLD', canvas.width / 2, 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add info overlay
|
||||||
|
this.addInfoOverlay(ctx, canvas, padding);
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawFilmPerforations(ctx, width, height, padding) {
|
||||||
|
ctx.fillStyle = '#333';
|
||||||
|
const perfSize = 15;
|
||||||
|
const perfSpacing = 30;
|
||||||
|
|
||||||
|
// Left perforations
|
||||||
|
for (let y = perfSpacing; y < height; y += perfSpacing) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(8, y, perfSize, perfSize, 3);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right perforations
|
||||||
|
for (let y = perfSpacing; y < height; y += perfSpacing) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(width - 23, y, perfSize, perfSize, 3);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addInfoOverlay(ctx, canvas, padding) {
|
||||||
|
const showLocation = document.getElementById('show-location')?.checked ?? true;
|
||||||
|
const showCoords = document.getElementById('show-coords')?.checked ?? true;
|
||||||
|
const showDate = document.getElementById('show-date')?.checked ?? true;
|
||||||
|
const showWatermark = document.getElementById('show-watermark')?.checked ?? true;
|
||||||
|
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
|
||||||
|
ctx.font = '14px "Exo 2", sans-serif';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
|
||||||
|
const bottom = canvas.height - 15;
|
||||||
|
let textY = bottom;
|
||||||
|
|
||||||
|
// Location name
|
||||||
|
if (showLocation) {
|
||||||
|
const locationName = document.getElementById('location-name')?.textContent || 'Unknown Location';
|
||||||
|
ctx.fillText(`📍 ${locationName}`, padding.left + 10, textY);
|
||||||
|
textY -= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coordinates
|
||||||
|
if (showCoords) {
|
||||||
|
const center = this.mapManager.getCenter();
|
||||||
|
ctx.font = '12px "Exo 2", sans-serif';
|
||||||
|
ctx.fillText(`${center.lat.toFixed(4)}°, ${center.lng.toFixed(4)}°`, padding.left + 10, textY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
if (showDate) {
|
||||||
|
ctx.textAlign = 'right';
|
||||||
|
ctx.font = '12px "Exo 2", sans-serif';
|
||||||
|
const date = new Date().toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
ctx.fillText(date, canvas.width - padding.right - 10, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watermark
|
||||||
|
if (showWatermark) {
|
||||||
|
ctx.textAlign = 'right';
|
||||||
|
ctx.fillStyle = 'rgba(0, 212, 255, 0.7)';
|
||||||
|
ctx.font = 'bold 12px Orbitron, sans-serif';
|
||||||
|
ctx.fillText('ROADWORLD', canvas.width - padding.right - 10, padding.top + 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadPhoto() {
|
||||||
|
if (!this.currentPhoto) return;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `roadworld_${Date.now()}.png`;
|
||||||
|
link.href = this.currentPhoto.toDataURL('image/png');
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
this.showNotification('📥 Photo saved!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async sharePhoto() {
|
||||||
|
if (!this.currentPhoto) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await new Promise(resolve => {
|
||||||
|
this.currentPhoto.toBlob(resolve, 'image/png');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (navigator.share && navigator.canShare({ files: [new File([blob], 'roadworld.png')] })) {
|
||||||
|
await navigator.share({
|
||||||
|
files: [new File([blob], 'roadworld.png', { type: 'image/png' })],
|
||||||
|
title: 'RoadWorld Photo',
|
||||||
|
text: 'Check out this view from RoadWorld!'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: copy to clipboard
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({ 'image/png': blob })
|
||||||
|
]);
|
||||||
|
this.showNotification('📋 Copied to clipboard!');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showNotification('Share failed');
|
||||||
|
console.error('Share error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addToGallery() {
|
||||||
|
if (!this.currentPhoto) return;
|
||||||
|
|
||||||
|
const photoData = {
|
||||||
|
id: Date.now(),
|
||||||
|
dataUrl: this.currentPhoto.toDataURL('image/jpeg', 0.7),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
location: document.getElementById('location-name')?.textContent || 'Unknown'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.photoGallery.unshift(photoData);
|
||||||
|
|
||||||
|
// Limit gallery size
|
||||||
|
if (this.photoGallery.length > 20) {
|
||||||
|
this.photoGallery.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveGallery();
|
||||||
|
this.renderGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGallery() {
|
||||||
|
const saved = this.storageManager.data.photoGallery || [];
|
||||||
|
this.photoGallery = saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveGallery() {
|
||||||
|
this.storageManager.data.photoGallery = this.photoGallery;
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGallery() {
|
||||||
|
const gallery = document.getElementById('photo-gallery');
|
||||||
|
if (!gallery) return;
|
||||||
|
|
||||||
|
if (this.photoGallery.length === 0) {
|
||||||
|
gallery.innerHTML = '<div class="no-photos">No photos yet. Start capturing!</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gallery.innerHTML = this.photoGallery.map(photo => `
|
||||||
|
<div class="gallery-item" data-id="${photo.id}">
|
||||||
|
<img src="${photo.dataUrl}" alt="${photo.location}">
|
||||||
|
<div class="gallery-info">
|
||||||
|
<span>${photo.location}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
gallery.querySelectorAll('.gallery-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const photo = this.photoGallery.find(p => p.id === parseInt(item.dataset.id));
|
||||||
|
if (photo) {
|
||||||
|
this.showFullPhoto(photo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showFullPhoto(photo) {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'photo-fullscreen';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<img src="${photo.dataUrl}" alt="${photo.location}">
|
||||||
|
<div class="photo-fullscreen-info">
|
||||||
|
<span>${photo.location}</span>
|
||||||
|
<span>${new Date(photo.timestamp).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
<button class="photo-fullscreen-close">✕</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
overlay.querySelector('.photo-fullscreen-close').addEventListener('click', () => {
|
||||||
|
overlay.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) overlay.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
414
src/js/skillTree.js
Normal file
414
src/js/skillTree.js
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
// Skill Tree System for RoadWorld
|
||||||
|
// Upgrade and specialize your explorer abilities
|
||||||
|
|
||||||
|
export class SkillTree {
|
||||||
|
constructor(gameEngine, storageManager) {
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
this.skillPoints = 0;
|
||||||
|
this.unlockedSkills = new Set();
|
||||||
|
this.panelElement = null;
|
||||||
|
this.isVisible = false;
|
||||||
|
|
||||||
|
// Skill categories and definitions
|
||||||
|
this.skills = {
|
||||||
|
// Explorer Branch
|
||||||
|
swiftFeet: {
|
||||||
|
id: 'swiftFeet',
|
||||||
|
name: 'Swift Feet',
|
||||||
|
icon: '👟',
|
||||||
|
branch: 'explorer',
|
||||||
|
tier: 1,
|
||||||
|
description: '+10% movement speed',
|
||||||
|
effect: { type: 'speed', value: 1.1 },
|
||||||
|
cost: 1,
|
||||||
|
requires: []
|
||||||
|
},
|
||||||
|
marathonRunner: {
|
||||||
|
id: 'marathonRunner',
|
||||||
|
name: 'Marathon Runner',
|
||||||
|
icon: '🏃',
|
||||||
|
branch: 'explorer',
|
||||||
|
tier: 2,
|
||||||
|
description: '+25% movement speed',
|
||||||
|
effect: { type: 'speed', value: 1.25 },
|
||||||
|
cost: 2,
|
||||||
|
requires: ['swiftFeet']
|
||||||
|
},
|
||||||
|
teleporter: {
|
||||||
|
id: 'teleporter',
|
||||||
|
name: 'Teleporter',
|
||||||
|
icon: '⚡',
|
||||||
|
branch: 'explorer',
|
||||||
|
tier: 3,
|
||||||
|
description: 'Quick travel cooldown -50%',
|
||||||
|
effect: { type: 'travelCooldown', value: 0.5 },
|
||||||
|
cost: 3,
|
||||||
|
requires: ['marathonRunner']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Collector Branch
|
||||||
|
keenEye: {
|
||||||
|
id: 'keenEye',
|
||||||
|
name: 'Keen Eye',
|
||||||
|
icon: '👁️',
|
||||||
|
branch: 'collector',
|
||||||
|
tier: 1,
|
||||||
|
description: '+15% item visibility range',
|
||||||
|
effect: { type: 'itemRange', value: 1.15 },
|
||||||
|
cost: 1,
|
||||||
|
requires: []
|
||||||
|
},
|
||||||
|
treasureHunter: {
|
||||||
|
id: 'treasureHunter',
|
||||||
|
name: 'Treasure Hunter',
|
||||||
|
icon: '💎',
|
||||||
|
branch: 'collector',
|
||||||
|
tier: 2,
|
||||||
|
description: '+25% rare item chance',
|
||||||
|
effect: { type: 'rareChance', value: 1.25 },
|
||||||
|
cost: 2,
|
||||||
|
requires: ['keenEye']
|
||||||
|
},
|
||||||
|
magneticAura: {
|
||||||
|
id: 'magneticAura',
|
||||||
|
name: 'Magnetic Aura',
|
||||||
|
icon: '🧲',
|
||||||
|
branch: 'collector',
|
||||||
|
tier: 3,
|
||||||
|
description: 'Auto-collect items within 30m',
|
||||||
|
effect: { type: 'autoCollect', value: 30 },
|
||||||
|
cost: 3,
|
||||||
|
requires: ['treasureHunter']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Scholar Branch
|
||||||
|
quickLearner: {
|
||||||
|
id: 'quickLearner',
|
||||||
|
name: 'Quick Learner',
|
||||||
|
icon: '📚',
|
||||||
|
branch: 'scholar',
|
||||||
|
tier: 1,
|
||||||
|
description: '+10% XP from all sources',
|
||||||
|
effect: { type: 'xp', value: 1.1 },
|
||||||
|
cost: 1,
|
||||||
|
requires: []
|
||||||
|
},
|
||||||
|
wisdomSeeker: {
|
||||||
|
id: 'wisdomSeeker',
|
||||||
|
name: 'Wisdom Seeker',
|
||||||
|
icon: '🎓',
|
||||||
|
branch: 'scholar',
|
||||||
|
tier: 2,
|
||||||
|
description: '+25% XP from all sources',
|
||||||
|
effect: { type: 'xp', value: 1.25 },
|
||||||
|
cost: 2,
|
||||||
|
requires: ['quickLearner']
|
||||||
|
},
|
||||||
|
enlightened: {
|
||||||
|
id: 'enlightened',
|
||||||
|
name: 'Enlightened',
|
||||||
|
icon: '✨',
|
||||||
|
branch: 'scholar',
|
||||||
|
tier: 3,
|
||||||
|
description: '+50% XP, +1 skill point per 5 levels',
|
||||||
|
effect: { type: 'xp', value: 1.5 },
|
||||||
|
cost: 3,
|
||||||
|
requires: ['wisdomSeeker']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Survivor Branch
|
||||||
|
endurance: {
|
||||||
|
id: 'endurance',
|
||||||
|
name: 'Endurance',
|
||||||
|
icon: '❤️',
|
||||||
|
branch: 'survivor',
|
||||||
|
tier: 1,
|
||||||
|
description: 'Sprint duration +50%',
|
||||||
|
effect: { type: 'sprintDuration', value: 1.5 },
|
||||||
|
cost: 1,
|
||||||
|
requires: []
|
||||||
|
},
|
||||||
|
nightOwl: {
|
||||||
|
id: 'nightOwl',
|
||||||
|
name: 'Night Owl',
|
||||||
|
icon: '🦉',
|
||||||
|
branch: 'survivor',
|
||||||
|
tier: 2,
|
||||||
|
description: '+50% night time bonuses',
|
||||||
|
effect: { type: 'nightBonus', value: 1.5 },
|
||||||
|
cost: 2,
|
||||||
|
requires: ['endurance']
|
||||||
|
},
|
||||||
|
weatherProof: {
|
||||||
|
id: 'weatherProof',
|
||||||
|
name: 'Weather Proof',
|
||||||
|
icon: '☔',
|
||||||
|
branch: 'survivor',
|
||||||
|
tier: 3,
|
||||||
|
description: 'Bonus XP during weather events',
|
||||||
|
effect: { type: 'weatherBonus', value: 1.3 },
|
||||||
|
cost: 3,
|
||||||
|
requires: ['nightOwl']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mystic Branch
|
||||||
|
luckyStar: {
|
||||||
|
id: 'luckyStar',
|
||||||
|
name: 'Lucky Star',
|
||||||
|
icon: '🍀',
|
||||||
|
branch: 'mystic',
|
||||||
|
tier: 1,
|
||||||
|
description: '+10% critical collection chance',
|
||||||
|
effect: { type: 'critChance', value: 0.1 },
|
||||||
|
cost: 1,
|
||||||
|
requires: []
|
||||||
|
},
|
||||||
|
fortuneFavored: {
|
||||||
|
id: 'fortuneFavored',
|
||||||
|
name: 'Fortune Favored',
|
||||||
|
icon: '🎰',
|
||||||
|
branch: 'mystic',
|
||||||
|
tier: 2,
|
||||||
|
description: '+20% power-up drop rate',
|
||||||
|
effect: { type: 'powerupDrop', value: 1.2 },
|
||||||
|
cost: 2,
|
||||||
|
requires: ['luckyStar']
|
||||||
|
},
|
||||||
|
goldTouch: {
|
||||||
|
id: 'goldTouch',
|
||||||
|
name: 'Gold Touch',
|
||||||
|
icon: '👑',
|
||||||
|
branch: 'mystic',
|
||||||
|
tier: 3,
|
||||||
|
description: 'Chance to double all rewards',
|
||||||
|
effect: { type: 'doubleRewards', value: 0.1 },
|
||||||
|
cost: 3,
|
||||||
|
requires: ['fortuneFavored']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Branch colors
|
||||||
|
this.branchColors = {
|
||||||
|
explorer: '#00d4ff',
|
||||||
|
collector: '#FFD700',
|
||||||
|
scholar: '#9b59b6',
|
||||||
|
survivor: '#e74c3c',
|
||||||
|
mystic: '#2ecc71'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.loadProgress();
|
||||||
|
|
||||||
|
console.log('🌳 Skill Tree initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'skill-tree-panel ui-overlay';
|
||||||
|
this.panelElement.id = 'skill-tree-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>🌳 Skill Tree</span>
|
||||||
|
<button class="panel-close" id="skills-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content skill-content">
|
||||||
|
<div class="skill-points-display">
|
||||||
|
<span class="skill-points-label">Skill Points:</span>
|
||||||
|
<span class="skill-points-value" id="skill-points-value">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-branches" id="skill-branches"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('skills-close').addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProgress() {
|
||||||
|
const saved = this.storageManager.data.skillTree || {};
|
||||||
|
this.skillPoints = saved.points || 0;
|
||||||
|
this.unlockedSkills = new Set(saved.unlocked || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProgress() {
|
||||||
|
this.storageManager.data.skillTree = {
|
||||||
|
points: this.skillPoints,
|
||||||
|
unlocked: Array.from(this.unlockedSkills)
|
||||||
|
};
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
addSkillPoints(amount) {
|
||||||
|
this.skillPoints += amount;
|
||||||
|
this.saveProgress();
|
||||||
|
this.updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
canUnlock(skillId) {
|
||||||
|
const skill = this.skills[skillId];
|
||||||
|
if (!skill) return false;
|
||||||
|
if (this.unlockedSkills.has(skillId)) return false;
|
||||||
|
if (this.skillPoints < skill.cost) return false;
|
||||||
|
|
||||||
|
// Check requirements
|
||||||
|
return skill.requires.every(reqId => this.unlockedSkills.has(reqId));
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockSkill(skillId) {
|
||||||
|
if (!this.canUnlock(skillId)) return false;
|
||||||
|
|
||||||
|
const skill = this.skills[skillId];
|
||||||
|
this.skillPoints -= skill.cost;
|
||||||
|
this.unlockedSkills.add(skillId);
|
||||||
|
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderPanel();
|
||||||
|
|
||||||
|
this.showNotification(`🌟 Skill unlocked: ${skill.name}!`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSkillBonus(effectType) {
|
||||||
|
let totalBonus = 1;
|
||||||
|
|
||||||
|
this.unlockedSkills.forEach(skillId => {
|
||||||
|
const skill = this.skills[skillId];
|
||||||
|
if (skill?.effect.type === effectType) {
|
||||||
|
if (effectType === 'critChance' || effectType === 'doubleRewards') {
|
||||||
|
totalBonus += skill.effect.value;
|
||||||
|
} else {
|
||||||
|
totalBonus *= skill.effect.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return totalBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSkill(skillId) {
|
||||||
|
return this.unlockedSkills.has(skillId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAutoCollectRange() {
|
||||||
|
if (this.hasSkill('magneticAura')) {
|
||||||
|
return this.skills.magneticAura.effect.value;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when player levels up
|
||||||
|
onLevelUp(newLevel) {
|
||||||
|
// Award skill point every level
|
||||||
|
this.addSkillPoints(1);
|
||||||
|
|
||||||
|
// Bonus point every 5 levels if enlightened
|
||||||
|
if (this.hasSkill('enlightened') && newLevel % 5 === 0) {
|
||||||
|
this.addSkillPoints(1);
|
||||||
|
this.showNotification('🎓 Enlightened bonus: +1 Skill Point!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
const pointsEl = document.getElementById('skill-points-value');
|
||||||
|
if (pointsEl) {
|
||||||
|
pointsEl.textContent = this.skillPoints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPanel() {
|
||||||
|
this.updateDisplay();
|
||||||
|
|
||||||
|
const branchesEl = document.getElementById('skill-branches');
|
||||||
|
if (!branchesEl) return;
|
||||||
|
|
||||||
|
const branches = ['explorer', 'collector', 'scholar', 'survivor', 'mystic'];
|
||||||
|
|
||||||
|
branchesEl.innerHTML = branches.map(branch => {
|
||||||
|
const branchSkills = Object.values(this.skills)
|
||||||
|
.filter(s => s.branch === branch)
|
||||||
|
.sort((a, b) => a.tier - b.tier);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="skill-branch" style="--branch-color: ${this.branchColors[branch]}">
|
||||||
|
<div class="branch-header">
|
||||||
|
<span class="branch-name">${branch.charAt(0).toUpperCase() + branch.slice(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="branch-skills">
|
||||||
|
${branchSkills.map(skill => {
|
||||||
|
const isUnlocked = this.unlockedSkills.has(skill.id);
|
||||||
|
const canUnlock = this.canUnlock(skill.id);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="skill-node ${isUnlocked ? 'unlocked' : ''} ${canUnlock ? 'available' : ''}"
|
||||||
|
data-skill="${skill.id}">
|
||||||
|
<div class="skill-icon">${skill.icon}</div>
|
||||||
|
<div class="skill-tooltip">
|
||||||
|
<div class="tooltip-name">${skill.name}</div>
|
||||||
|
<div class="tooltip-desc">${skill.description}</div>
|
||||||
|
<div class="tooltip-cost">Cost: ${skill.cost} SP</div>
|
||||||
|
${skill.requires.length > 0 ? `
|
||||||
|
<div class="tooltip-requires">
|
||||||
|
Requires: ${skill.requires.map(r => this.skills[r]?.name).join(', ')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Add click handlers
|
||||||
|
branchesEl.querySelectorAll('.skill-node.available').forEach(node => {
|
||||||
|
node.addEventListener('click', () => {
|
||||||
|
const skillId = node.dataset.skill;
|
||||||
|
this.unlockSkill(skillId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isVisible = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
481
src/js/treasureHunt.js
Normal file
481
src/js/treasureHunt.js
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
// Treasure Hunt System for RoadWorld
|
||||||
|
// Mystery quests and treasure hunts across the map
|
||||||
|
|
||||||
|
export class TreasureHunt {
|
||||||
|
constructor(mapManager, gameEngine, storageManager) {
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
this.activeHunt = null;
|
||||||
|
this.completedHunts = [];
|
||||||
|
this.huntMarkers = [];
|
||||||
|
this.panelElement = null;
|
||||||
|
this.isVisible = false;
|
||||||
|
|
||||||
|
// Treasure hunt templates
|
||||||
|
this.huntTemplates = {
|
||||||
|
worldWonder: {
|
||||||
|
id: 'worldWonder',
|
||||||
|
name: 'Seven Wonders Quest',
|
||||||
|
icon: '🏛️',
|
||||||
|
description: 'Visit the Seven Wonders of the World!',
|
||||||
|
rarity: 'legendary',
|
||||||
|
waypoints: [
|
||||||
|
{ name: 'Great Pyramid', lat: 29.9792, lng: 31.1342, icon: '🏜️' },
|
||||||
|
{ name: 'Colosseum', lat: 41.8902, lng: 12.4922, icon: '🏟️' },
|
||||||
|
{ name: 'Machu Picchu', lat: -13.1631, lng: -72.5450, icon: '🏔️' },
|
||||||
|
{ name: 'Taj Mahal', lat: 27.1751, lng: 78.0421, icon: '🕌' },
|
||||||
|
{ name: 'Great Wall', lat: 40.4319, lng: 116.5704, icon: '🧱' },
|
||||||
|
{ name: 'Christ the Redeemer', lat: -22.9519, lng: -43.2105, icon: '✝️' },
|
||||||
|
{ name: 'Petra', lat: 30.3285, lng: 35.4444, icon: '🏺' }
|
||||||
|
],
|
||||||
|
rewards: { xp: 5000, items: { trophies: 10, gems: 20 } }
|
||||||
|
},
|
||||||
|
capitalCrawl: {
|
||||||
|
id: 'capitalCrawl',
|
||||||
|
name: 'Capital Explorer',
|
||||||
|
icon: '🏛️',
|
||||||
|
description: 'Visit 5 major world capitals!',
|
||||||
|
rarity: 'epic',
|
||||||
|
waypoints: [
|
||||||
|
{ name: 'Washington D.C.', lat: 38.8951, lng: -77.0364, icon: '🇺🇸' },
|
||||||
|
{ name: 'London', lat: 51.5074, lng: -0.1278, icon: '🇬🇧' },
|
||||||
|
{ name: 'Tokyo', lat: 35.6762, lng: 139.6503, icon: '🇯🇵' },
|
||||||
|
{ name: 'Paris', lat: 48.8566, lng: 2.3522, icon: '🇫🇷' },
|
||||||
|
{ name: 'Beijing', lat: 39.9042, lng: 116.4074, icon: '🇨🇳' }
|
||||||
|
],
|
||||||
|
rewards: { xp: 3000, items: { trophies: 5, gems: 10 } }
|
||||||
|
},
|
||||||
|
naturePilgrimage: {
|
||||||
|
id: 'naturePilgrimage',
|
||||||
|
name: 'Nature Pilgrimage',
|
||||||
|
icon: '🌲',
|
||||||
|
description: 'Explore natural wonders!',
|
||||||
|
rarity: 'epic',
|
||||||
|
waypoints: [
|
||||||
|
{ name: 'Grand Canyon', lat: 36.1069, lng: -112.1129, icon: '🏜️' },
|
||||||
|
{ name: 'Victoria Falls', lat: -17.9243, lng: 25.8572, icon: '💧' },
|
||||||
|
{ name: 'Mount Everest', lat: 27.9881, lng: 86.9250, icon: '🏔️' },
|
||||||
|
{ name: 'Amazon River', lat: -3.4653, lng: -62.2159, icon: '🌿' },
|
||||||
|
{ name: 'Great Barrier Reef', lat: -18.2871, lng: 147.6992, icon: '🐠' }
|
||||||
|
],
|
||||||
|
rewards: { xp: 2500, items: { gems: 8, stars: 15 } }
|
||||||
|
},
|
||||||
|
mysteryIslands: {
|
||||||
|
id: 'mysteryIslands',
|
||||||
|
name: 'Island Mysteries',
|
||||||
|
icon: '🏝️',
|
||||||
|
description: 'Discover mysterious islands!',
|
||||||
|
rarity: 'rare',
|
||||||
|
waypoints: [
|
||||||
|
{ name: 'Easter Island', lat: -27.1127, lng: -109.3497, icon: '🗿' },
|
||||||
|
{ name: 'Bermuda', lat: 32.3078, lng: -64.7505, icon: '🔺' },
|
||||||
|
{ name: 'Iceland', lat: 64.9631, lng: -19.0208, icon: '❄️' },
|
||||||
|
{ name: 'Galapagos', lat: -0.9538, lng: -90.9656, icon: '🐢' }
|
||||||
|
],
|
||||||
|
rewards: { xp: 2000, items: { keys: 5, gems: 5 } }
|
||||||
|
},
|
||||||
|
urbanExplorer: {
|
||||||
|
id: 'urbanExplorer',
|
||||||
|
name: 'Urban Explorer',
|
||||||
|
icon: '🌆',
|
||||||
|
description: 'Visit famous city landmarks!',
|
||||||
|
rarity: 'common',
|
||||||
|
waypoints: [
|
||||||
|
{ name: 'Times Square', lat: 40.7580, lng: -73.9855, icon: '🗽' },
|
||||||
|
{ name: 'Shibuya Crossing', lat: 35.6595, lng: 139.7004, icon: '🚶' },
|
||||||
|
{ name: 'Champs-Élysées', lat: 48.8698, lng: 2.3075, icon: '🗼' }
|
||||||
|
],
|
||||||
|
rewards: { xp: 1000, items: { stars: 10 } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Proximity threshold (in meters)
|
||||||
|
this.proximityThreshold = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.loadProgress();
|
||||||
|
|
||||||
|
console.log('🗺️ Treasure Hunt System initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'treasure-hunt-panel ui-overlay';
|
||||||
|
this.panelElement.id = 'treasure-hunt-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>🗺️ Treasure Hunts</span>
|
||||||
|
<button class="panel-close" id="hunt-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content hunt-content">
|
||||||
|
<div class="hunt-section" id="active-hunt-section">
|
||||||
|
<h4>🎯 Active Hunt</h4>
|
||||||
|
<div class="active-hunt-display" id="active-hunt-display"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hunt-section">
|
||||||
|
<h4>📜 Available Hunts</h4>
|
||||||
|
<div class="hunt-list" id="hunt-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hunt-section">
|
||||||
|
<h4>🏆 Completed</h4>
|
||||||
|
<div class="completed-hunts" id="completed-hunts"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('hunt-close').addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProgress() {
|
||||||
|
const saved = this.storageManager.data.treasureHunts || {};
|
||||||
|
this.completedHunts = saved.completed || [];
|
||||||
|
|
||||||
|
if (saved.active) {
|
||||||
|
this.activeHunt = saved.active;
|
||||||
|
this.updateMarkers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProgress() {
|
||||||
|
this.storageManager.data.treasureHunts = {
|
||||||
|
completed: this.completedHunts,
|
||||||
|
active: this.activeHunt
|
||||||
|
};
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
startHunt(huntId) {
|
||||||
|
const template = this.huntTemplates[huntId];
|
||||||
|
if (!template) return;
|
||||||
|
|
||||||
|
if (this.completedHunts.includes(huntId)) {
|
||||||
|
this.showNotification('You have already completed this hunt!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeHunt = {
|
||||||
|
...template,
|
||||||
|
startTime: Date.now(),
|
||||||
|
visitedWaypoints: [],
|
||||||
|
currentWaypoint: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateMarkers();
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderPanel();
|
||||||
|
|
||||||
|
this.showNotification(`🗺️ Started: ${template.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
abandonHunt() {
|
||||||
|
this.clearMarkers();
|
||||||
|
this.activeHunt = null;
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMarkers() {
|
||||||
|
this.clearMarkers();
|
||||||
|
|
||||||
|
if (!this.activeHunt) return;
|
||||||
|
|
||||||
|
this.activeHunt.waypoints.forEach((wp, index) => {
|
||||||
|
const isVisited = this.activeHunt.visitedWaypoints.includes(index);
|
||||||
|
const isCurrent = index === this.activeHunt.currentWaypoint;
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `hunt-marker ${isVisited ? 'visited' : ''} ${isCurrent ? 'current' : ''}`;
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="hunt-marker-icon">${wp.icon}</div>
|
||||||
|
<div class="hunt-marker-number">${index + 1}</div>
|
||||||
|
${isVisited ? '<div class="hunt-marker-check">✓</div>' : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const marker = new maplibregl.Marker({ element: el, anchor: 'center' })
|
||||||
|
.setLngLat([wp.lng, wp.lat])
|
||||||
|
.addTo(this.mapManager.map);
|
||||||
|
|
||||||
|
this.huntMarkers.push(marker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMarkers() {
|
||||||
|
this.huntMarkers.forEach(marker => marker.remove());
|
||||||
|
this.huntMarkers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWaypointProximity(playerLngLat) {
|
||||||
|
if (!this.activeHunt) return;
|
||||||
|
|
||||||
|
this.activeHunt.waypoints.forEach((wp, index) => {
|
||||||
|
if (this.activeHunt.visitedWaypoints.includes(index)) return;
|
||||||
|
|
||||||
|
const distance = this.calculateDistance(
|
||||||
|
playerLngLat[1], playerLngLat[0],
|
||||||
|
wp.lat, wp.lng
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance <= this.proximityThreshold) {
|
||||||
|
this.visitWaypoint(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
visitWaypoint(index) {
|
||||||
|
if (!this.activeHunt || this.activeHunt.visitedWaypoints.includes(index)) return;
|
||||||
|
|
||||||
|
const wp = this.activeHunt.waypoints[index];
|
||||||
|
this.activeHunt.visitedWaypoints.push(index);
|
||||||
|
|
||||||
|
// Find next unvisited waypoint
|
||||||
|
for (let i = 0; i < this.activeHunt.waypoints.length; i++) {
|
||||||
|
if (!this.activeHunt.visitedWaypoints.includes(i)) {
|
||||||
|
this.activeHunt.currentWaypoint = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showNotification(`📍 Discovered: ${wp.name}!`);
|
||||||
|
|
||||||
|
// Award partial XP
|
||||||
|
const partialXP = Math.floor(this.activeHunt.rewards.xp / this.activeHunt.waypoints.length);
|
||||||
|
if (this.gameEngine) {
|
||||||
|
this.gameEngine.addXP(partialXP, 'exploration');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check completion
|
||||||
|
if (this.activeHunt.visitedWaypoints.length === this.activeHunt.waypoints.length) {
|
||||||
|
this.completeHunt();
|
||||||
|
} else {
|
||||||
|
this.updateMarkers();
|
||||||
|
this.saveProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completeHunt() {
|
||||||
|
const hunt = this.activeHunt;
|
||||||
|
this.completedHunts.push(hunt.id);
|
||||||
|
|
||||||
|
// Award remaining rewards
|
||||||
|
if (this.gameEngine) {
|
||||||
|
this.gameEngine.addXP(Math.floor(hunt.rewards.xp / 2), 'quest');
|
||||||
|
|
||||||
|
if (hunt.rewards.items) {
|
||||||
|
Object.entries(hunt.rewards.items).forEach(([type, count]) => {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.gameEngine.collectItem({ type, rarity: 'quest' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showCompletionScreen(hunt);
|
||||||
|
this.clearMarkers();
|
||||||
|
this.activeHunt = null;
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
showCompletionScreen(hunt) {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'hunt-complete-overlay';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="hunt-complete-content">
|
||||||
|
<div class="hunt-complete-icon">🏆</div>
|
||||||
|
<div class="hunt-complete-title">Quest Complete!</div>
|
||||||
|
<div class="hunt-complete-name">${hunt.name}</div>
|
||||||
|
<div class="hunt-complete-rewards">
|
||||||
|
<div class="reward-xp">+${hunt.rewards.xp} XP</div>
|
||||||
|
${hunt.rewards.items ? `
|
||||||
|
<div class="reward-items">
|
||||||
|
${Object.entries(hunt.rewards.items).map(([type, count]) =>
|
||||||
|
`<span>${this.getItemIcon(type)} x${count}</span>`
|
||||||
|
).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<button class="hunt-complete-btn" id="hunt-complete-close">Continue</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
document.getElementById('hunt-complete-close').addEventListener('click', () => {
|
||||||
|
overlay.classList.add('fade-out');
|
||||||
|
setTimeout(() => overlay.remove(), 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemIcon(type) {
|
||||||
|
const icons = { stars: '⭐', gems: '💎', trophies: '🏆', keys: '🗝️' };
|
||||||
|
return icons[type] || '✨';
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDistance(lat1, lon1, lat2, lon2) {
|
||||||
|
const R = 6371000; // Earth's radius in meters
|
||||||
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||||
|
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||||
|
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||||
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||||
|
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
flyToNextWaypoint() {
|
||||||
|
if (!this.activeHunt) return;
|
||||||
|
|
||||||
|
const wp = this.activeHunt.waypoints[this.activeHunt.currentWaypoint];
|
||||||
|
this.mapManager.flyTo({
|
||||||
|
center: [wp.lng, wp.lat],
|
||||||
|
zoom: 16,
|
||||||
|
pitch: 60,
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPanel() {
|
||||||
|
// Active hunt
|
||||||
|
const activeDisplay = document.getElementById('active-hunt-display');
|
||||||
|
if (activeDisplay) {
|
||||||
|
if (this.activeHunt) {
|
||||||
|
const progress = this.activeHunt.visitedWaypoints.length;
|
||||||
|
const total = this.activeHunt.waypoints.length;
|
||||||
|
const currentWp = this.activeHunt.waypoints[this.activeHunt.currentWaypoint];
|
||||||
|
|
||||||
|
activeDisplay.innerHTML = `
|
||||||
|
<div class="active-hunt-card ${this.activeHunt.rarity}">
|
||||||
|
<div class="hunt-header">
|
||||||
|
<span class="hunt-icon">${this.activeHunt.icon}</span>
|
||||||
|
<span class="hunt-name">${this.activeHunt.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hunt-progress-info">
|
||||||
|
<div class="hunt-progress-bar">
|
||||||
|
<div class="hunt-progress-fill" style="width: ${(progress/total)*100}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hunt-progress-text">${progress}/${total} waypoints</div>
|
||||||
|
</div>
|
||||||
|
<div class="hunt-current">
|
||||||
|
<div class="hunt-current-label">Next Destination:</div>
|
||||||
|
<div class="hunt-current-name">${currentWp.icon} ${currentWp.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="hunt-actions">
|
||||||
|
<button class="hunt-fly-btn" id="fly-to-waypoint">🚀 Fly There</button>
|
||||||
|
<button class="hunt-abandon-btn" id="abandon-hunt">Abandon</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('fly-to-waypoint')?.addEventListener('click', () => {
|
||||||
|
this.flyToNextWaypoint();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('abandon-hunt')?.addEventListener('click', () => {
|
||||||
|
if (confirm('Abandon this treasure hunt?')) {
|
||||||
|
this.abandonHunt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activeDisplay.innerHTML = `
|
||||||
|
<div class="no-active-hunt">No active hunt. Choose one below!</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available hunts
|
||||||
|
const huntList = document.getElementById('hunt-list');
|
||||||
|
if (huntList) {
|
||||||
|
const available = Object.entries(this.huntTemplates)
|
||||||
|
.filter(([id]) => !this.completedHunts.includes(id) && this.activeHunt?.id !== id);
|
||||||
|
|
||||||
|
huntList.innerHTML = available.map(([id, hunt]) => `
|
||||||
|
<div class="hunt-card ${hunt.rarity}" data-hunt="${id}">
|
||||||
|
<div class="hunt-card-icon">${hunt.icon}</div>
|
||||||
|
<div class="hunt-card-info">
|
||||||
|
<div class="hunt-card-name">${hunt.name}</div>
|
||||||
|
<div class="hunt-card-desc">${hunt.description}</div>
|
||||||
|
<div class="hunt-card-meta">
|
||||||
|
<span>${hunt.waypoints.length} waypoints</span>
|
||||||
|
<span>+${hunt.rewards.xp} XP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="hunt-start-btn">Start</button>
|
||||||
|
</div>
|
||||||
|
`).join('') || '<div class="no-hunts">All hunts started or completed!</div>';
|
||||||
|
|
||||||
|
huntList.querySelectorAll('.hunt-start-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const huntId = e.target.closest('.hunt-card').dataset.hunt;
|
||||||
|
if (!this.activeHunt) {
|
||||||
|
this.startHunt(huntId);
|
||||||
|
} else {
|
||||||
|
this.showNotification('Complete or abandon current hunt first!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completed hunts
|
||||||
|
const completedList = document.getElementById('completed-hunts');
|
||||||
|
if (completedList) {
|
||||||
|
if (this.completedHunts.length === 0) {
|
||||||
|
completedList.innerHTML = '<div class="no-completed">No completed hunts yet.</div>';
|
||||||
|
} else {
|
||||||
|
completedList.innerHTML = this.completedHunts.map(id => {
|
||||||
|
const hunt = this.huntTemplates[id];
|
||||||
|
return hunt ? `
|
||||||
|
<div class="completed-hunt-item ${hunt.rarity}">
|
||||||
|
<span class="completed-icon">${hunt.icon}</span>
|
||||||
|
<span class="completed-name">${hunt.name}</span>
|
||||||
|
<span class="completed-check">✓</span>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
this.renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isVisible = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
441
src/js/worldEvents.js
Normal file
441
src/js/worldEvents.js
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
// World Events System for RoadWorld
|
||||||
|
// Dynamic global events and challenges
|
||||||
|
|
||||||
|
export class WorldEvents {
|
||||||
|
constructor(gameEngine, mapManager, storageManager) {
|
||||||
|
this.gameEngine = gameEngine;
|
||||||
|
this.mapManager = mapManager;
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
|
||||||
|
this.activeEvents = [];
|
||||||
|
this.completedEvents = new Set();
|
||||||
|
this.panelElement = null;
|
||||||
|
this.isVisible = false;
|
||||||
|
|
||||||
|
// Event types
|
||||||
|
this.eventTypes = {
|
||||||
|
meteorShower: {
|
||||||
|
name: 'Meteor Shower',
|
||||||
|
icon: '☄️',
|
||||||
|
description: 'Collect falling stars for bonus XP!',
|
||||||
|
duration: 5 * 60 * 1000, // 5 minutes
|
||||||
|
rarity: 'epic',
|
||||||
|
rewards: { xp: 500, items: { stars: 10 } },
|
||||||
|
spawnRate: 0.01 // Per hour
|
||||||
|
},
|
||||||
|
treasureRush: {
|
||||||
|
name: 'Treasure Rush',
|
||||||
|
icon: '💰',
|
||||||
|
description: 'Treasures spawn everywhere!',
|
||||||
|
duration: 3 * 60 * 1000,
|
||||||
|
rarity: 'rare',
|
||||||
|
rewards: { xp: 300, items: { gems: 5 } },
|
||||||
|
spawnRate: 0.05
|
||||||
|
},
|
||||||
|
doubleTrouble: {
|
||||||
|
name: 'Double Trouble',
|
||||||
|
icon: '✨',
|
||||||
|
description: 'All XP rewards are doubled!',
|
||||||
|
duration: 2 * 60 * 1000,
|
||||||
|
rarity: 'common',
|
||||||
|
rewards: { xp: 200 },
|
||||||
|
spawnRate: 0.1
|
||||||
|
},
|
||||||
|
mysteryZone: {
|
||||||
|
name: 'Mystery Zone',
|
||||||
|
icon: '🔮',
|
||||||
|
description: 'A mysterious area has appeared nearby!',
|
||||||
|
duration: 10 * 60 * 1000,
|
||||||
|
rarity: 'legendary',
|
||||||
|
rewards: { xp: 1000, items: { keys: 3 } },
|
||||||
|
spawnRate: 0.005
|
||||||
|
},
|
||||||
|
speedDemon: {
|
||||||
|
name: 'Speed Demon',
|
||||||
|
icon: '⚡',
|
||||||
|
description: 'Move faster and gain speed bonuses!',
|
||||||
|
duration: 4 * 60 * 1000,
|
||||||
|
rarity: 'rare',
|
||||||
|
rewards: { xp: 250 },
|
||||||
|
spawnRate: 0.04
|
||||||
|
},
|
||||||
|
luckyHour: {
|
||||||
|
name: 'Lucky Hour',
|
||||||
|
icon: '🍀',
|
||||||
|
description: 'Increased rare item drop rates!',
|
||||||
|
duration: 60 * 60 * 1000, // 1 hour
|
||||||
|
rarity: 'epic',
|
||||||
|
rewards: { xp: 400, items: { trophies: 2 } },
|
||||||
|
spawnRate: 0.02
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Community challenges
|
||||||
|
this.challenges = {
|
||||||
|
globalDistance: {
|
||||||
|
name: 'World Tour',
|
||||||
|
icon: '🌍',
|
||||||
|
description: 'Community goal: Travel 1000km total',
|
||||||
|
target: 1000000, // meters
|
||||||
|
current: 0,
|
||||||
|
rewards: { xp: 2000, items: { trophies: 5 } }
|
||||||
|
},
|
||||||
|
globalItems: {
|
||||||
|
name: 'Treasure Hunters',
|
||||||
|
icon: '📦',
|
||||||
|
description: 'Community goal: Collect 10,000 items',
|
||||||
|
target: 10000,
|
||||||
|
current: 0,
|
||||||
|
rewards: { xp: 1500, items: { gems: 10 } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.checkInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createPanel();
|
||||||
|
this.loadProgress();
|
||||||
|
this.startEventChecker();
|
||||||
|
|
||||||
|
console.log('🌍 World Events initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanel() {
|
||||||
|
this.panelElement = document.createElement('div');
|
||||||
|
this.panelElement.className = 'world-events-panel ui-overlay';
|
||||||
|
this.panelElement.id = 'world-events-panel';
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
|
||||||
|
this.panelElement.innerHTML = `
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>🌍 World Events</span>
|
||||||
|
<button class="panel-close" id="events-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content events-content">
|
||||||
|
<div class="events-section">
|
||||||
|
<h4>🎯 Active Events</h4>
|
||||||
|
<div class="active-events-list" id="active-events-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="events-section">
|
||||||
|
<h4>🏆 Community Challenges</h4>
|
||||||
|
<div class="challenges-list" id="challenges-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="events-section">
|
||||||
|
<h4>📅 Upcoming</h4>
|
||||||
|
<div class="upcoming-events" id="upcoming-events">
|
||||||
|
<div class="event-hint">Events spawn randomly based on your activity!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(this.panelElement);
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.getElementById('events-close').addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProgress() {
|
||||||
|
const saved = this.storageManager.data.worldEvents || {};
|
||||||
|
this.completedEvents = new Set(saved.completed || []);
|
||||||
|
|
||||||
|
// Load challenge progress
|
||||||
|
if (saved.challenges) {
|
||||||
|
Object.keys(saved.challenges).forEach(key => {
|
||||||
|
if (this.challenges[key]) {
|
||||||
|
this.challenges[key].current = saved.challenges[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProgress() {
|
||||||
|
const challengeProgress = {};
|
||||||
|
Object.keys(this.challenges).forEach(key => {
|
||||||
|
challengeProgress[key] = this.challenges[key].current;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.storageManager.data.worldEvents = {
|
||||||
|
completed: Array.from(this.completedEvents),
|
||||||
|
challenges: challengeProgress
|
||||||
|
};
|
||||||
|
this.storageManager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEventChecker() {
|
||||||
|
// Check for new events every minute
|
||||||
|
this.checkInterval = setInterval(() => {
|
||||||
|
this.checkForNewEvents();
|
||||||
|
this.updateActiveEvents();
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
this.checkForNewEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForNewEvents() {
|
||||||
|
// Random chance to spawn events
|
||||||
|
Object.entries(this.eventTypes).forEach(([type, event]) => {
|
||||||
|
if (Math.random() < event.spawnRate / 60) {
|
||||||
|
this.startEvent(type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActiveEvents() {
|
||||||
|
const now = Date.now();
|
||||||
|
this.activeEvents = this.activeEvents.filter(event => {
|
||||||
|
if (now > event.endTime) {
|
||||||
|
this.onEventEnd(event);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
startEvent(type) {
|
||||||
|
const eventType = this.eventTypes[type];
|
||||||
|
if (!eventType) return;
|
||||||
|
|
||||||
|
// Check if event is already active
|
||||||
|
if (this.activeEvents.some(e => e.type === type)) return;
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
id: `${type}_${Date.now()}`,
|
||||||
|
type,
|
||||||
|
...eventType,
|
||||||
|
startTime: Date.now(),
|
||||||
|
endTime: Date.now() + eventType.duration,
|
||||||
|
progress: 0,
|
||||||
|
target: this.getEventTarget(type)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activeEvents.push(event);
|
||||||
|
this.showEventNotification(event, 'start');
|
||||||
|
this.renderEvents();
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventTarget(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'meteorShower': return 10;
|
||||||
|
case 'treasureRush': return 15;
|
||||||
|
case 'mysteryZone': return 1;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEventEnd(event) {
|
||||||
|
this.completedEvents.add(event.id);
|
||||||
|
this.showEventNotification(event, 'end');
|
||||||
|
|
||||||
|
// Award completion rewards if target met
|
||||||
|
if (event.target && event.progress >= event.target) {
|
||||||
|
this.awardEventRewards(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
awardEventRewards(event) {
|
||||||
|
if (this.gameEngine && event.rewards) {
|
||||||
|
if (event.rewards.xp) {
|
||||||
|
this.gameEngine.addXP(event.rewards.xp, 'event');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.rewards.items) {
|
||||||
|
Object.entries(event.rewards.items).forEach(([item, count]) => {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.gameEngine.collectItem({ type: item, rarity: 'event' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showNotification(`🎉 Event Complete! +${event.rewards.xp} XP`);
|
||||||
|
}
|
||||||
|
|
||||||
|
showEventNotification(event, type) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `event-notification ${event.rarity}`;
|
||||||
|
|
||||||
|
if (type === 'start') {
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="event-notif-icon">${event.icon}</div>
|
||||||
|
<div class="event-notif-info">
|
||||||
|
<div class="event-notif-title">${event.name} Started!</div>
|
||||||
|
<div class="event-notif-desc">${event.description}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="event-notif-icon">${event.icon}</div>
|
||||||
|
<div class="event-notif-info">
|
||||||
|
<div class="event-notif-title">${event.name} Ended</div>
|
||||||
|
<div class="event-notif-desc">Progress: ${event.progress}/${event.target || '∞'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => notification.classList.add('show'), 10);
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 500);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEventProgress(eventType, amount = 1) {
|
||||||
|
this.activeEvents.forEach(event => {
|
||||||
|
if (event.type === eventType && event.target) {
|
||||||
|
event.progress = Math.min(event.progress + amount, event.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.renderEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChallengeProgress(type, amount) {
|
||||||
|
if (this.challenges[type]) {
|
||||||
|
this.challenges[type].current = Math.min(
|
||||||
|
this.challenges[type].current + amount,
|
||||||
|
this.challenges[type].target
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check completion
|
||||||
|
if (this.challenges[type].current >= this.challenges[type].target) {
|
||||||
|
this.completeChallenge(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveProgress();
|
||||||
|
this.renderEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completeChallenge(type) {
|
||||||
|
const challenge = this.challenges[type];
|
||||||
|
this.showNotification(`🏆 Challenge Complete: ${challenge.name}!`);
|
||||||
|
this.awardEventRewards({ rewards: challenge.rewards });
|
||||||
|
|
||||||
|
// Reset for next round
|
||||||
|
challenge.current = 0;
|
||||||
|
challenge.target = Math.floor(challenge.target * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEventActive(type) {
|
||||||
|
return this.activeEvents.some(e => e.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveEventMultiplier() {
|
||||||
|
let multiplier = 1;
|
||||||
|
|
||||||
|
if (this.isEventActive('doubleTrouble')) multiplier *= 2;
|
||||||
|
if (this.isEventActive('luckyHour')) multiplier *= 1.5;
|
||||||
|
|
||||||
|
return multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEvents() {
|
||||||
|
// Active events
|
||||||
|
const activeList = document.getElementById('active-events-list');
|
||||||
|
if (!activeList) return;
|
||||||
|
|
||||||
|
if (this.activeEvents.length === 0) {
|
||||||
|
activeList.innerHTML = `
|
||||||
|
<div class="no-events">No active events. Keep exploring!</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
activeList.innerHTML = this.activeEvents.map(event => {
|
||||||
|
const remaining = Math.max(0, event.endTime - Date.now());
|
||||||
|
const minutes = Math.floor(remaining / 60000);
|
||||||
|
const seconds = Math.floor((remaining % 60000) / 1000);
|
||||||
|
const progressPercent = event.target ? (event.progress / event.target) * 100 : 100;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="event-card ${event.rarity}">
|
||||||
|
<div class="event-header">
|
||||||
|
<span class="event-icon">${event.icon}</span>
|
||||||
|
<span class="event-name">${event.name}</span>
|
||||||
|
<span class="event-timer">${minutes}:${seconds.toString().padStart(2, '0')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-desc">${event.description}</div>
|
||||||
|
${event.target ? `
|
||||||
|
<div class="event-progress">
|
||||||
|
<div class="event-progress-bar" style="width: ${progressPercent}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="event-progress-text">${event.progress}/${event.target}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Challenges
|
||||||
|
const challengesList = document.getElementById('challenges-list');
|
||||||
|
if (challengesList) {
|
||||||
|
challengesList.innerHTML = Object.entries(this.challenges).map(([key, challenge]) => {
|
||||||
|
const progressPercent = (challenge.current / challenge.target) * 100;
|
||||||
|
return `
|
||||||
|
<div class="challenge-card">
|
||||||
|
<div class="challenge-header">
|
||||||
|
<span class="challenge-icon">${challenge.icon}</span>
|
||||||
|
<span class="challenge-name">${challenge.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="challenge-desc">${challenge.description}</div>
|
||||||
|
<div class="challenge-progress">
|
||||||
|
<div class="challenge-progress-bar" style="width: ${progressPercent}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="challenge-progress-text">
|
||||||
|
${challenge.current.toLocaleString()}/${challenge.target.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.panelElement.style.display = 'block';
|
||||||
|
this.renderEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isVisible = false;
|
||||||
|
this.panelElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.checkInterval) {
|
||||||
|
clearInterval(this.checkInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user