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:
Claude
2025-12-28 06:14:45 +00:00
parent 7b4938e09b
commit 1fcbe163dc
11 changed files with 4882 additions and 2 deletions

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BlackRoad Earth | RoadWorld 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 -->

View File

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

View File

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