Add next-level features for RoadWorld v5.0

New Features:
- WASD keyboard controls with sprint (Shift)
- Daily login streak with escalating rewards
- Combo system for rapid item collections (up to 4x XP)
- Power-ups: XP boost, magnet, lucky, speed, shield, golden
- Screenshot capture with preview and download
- Local leaderboard with personal records
- Profile customization (username, avatar icon/color)
- Enhanced particle effects for collections and level-ups
- Quick travel to saved locations and famous landmarks

Integration:
- Updated main.js with all v5.0 feature imports and initialization
- Added v5.0 control buttons to UI
- Added 970+ lines of CSS for new components
- Combo multipliers stack with power-ups and streak bonuses
This commit is contained in:
Claude
2025-12-28 05:57:24 +00:00
parent c7f024948e
commit 7b4938e09b
12 changed files with 3469 additions and 8 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 v4.0</title> <title>BlackRoad Earth | RoadWorld v5.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>
@@ -160,6 +160,12 @@
<button class="ctrl-btn" id="btn-stats" title="Statistics">📊</button> <button class="ctrl-btn" id="btn-stats" title="Statistics">📊</button>
<button class="ctrl-btn active" id="btn-weather" title="Weather Effects">🌤️</button> <button class="ctrl-btn active" id="btn-weather" title="Weather Effects">🌤️</button>
<button class="ctrl-btn active" id="btn-sound" title="Sound">🔊</button> <button class="ctrl-btn active" id="btn-sound" title="Sound">🔊</button>
<div class="divider"></div>
<button class="ctrl-btn" id="btn-screenshot" title="Screenshot">📸</button>
<button class="ctrl-btn" id="btn-leaderboard" title="Leaderboard">🏆</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-keyboard" title="WASD Controls">⌨️</button>
</div> </div>
<!-- Instructions --> <!-- Instructions -->
@@ -168,6 +174,8 @@
<div><kbd>SCROLL</kbd> Zoom</div> <div><kbd>SCROLL</kbd> Zoom</div>
<div><kbd>CTRL+DRAG</kbd> Tilt</div> <div><kbd>CTRL+DRAG</kbd> Tilt</div>
<div><kbd>RIGHT-CLICK</kbd> Rotate</div> <div><kbd>RIGHT-CLICK</kbd> Rotate</div>
<div><kbd>WASD</kbd> Move (Game)</div>
<div><kbd>SHIFT</kbd> Sprint</div>
</div> </div>
<!-- Tools Panel --> <!-- Tools Panel -->

View File

@@ -2025,3 +2025,975 @@ body[data-time="night"] { }
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
} }
/* ========================================
v5.0 NEXT LEVEL FEATURES
======================================== */
/* Particle Effects Canvas */
.particle-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 500;
}
/* Combo System Display */
.combo-display {
position: fixed;
top: 50%;
right: 20px;
transform: translateY(-50%);
text-align: center;
z-index: 200;
pointer-events: none;
}
.combo-counter {
font-family: 'Orbitron', sans-serif;
font-size: 48px;
font-weight: 900;
color: #FFD700;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.8),
0 0 40px rgba(255, 215, 0, 0.5);
animation: comboPulse 0.3s ease-out;
}
.combo-label {
font-size: 14px;
letter-spacing: 3px;
text-transform: uppercase;
color: #fff;
opacity: 0.8;
}
.combo-multiplier {
font-family: 'Orbitron', sans-serif;
font-size: 24px;
color: #ff6b35;
margin-top: 4px;
}
.combo-timer {
width: 100px;
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
margin: 8px auto 0;
overflow: hidden;
}
.combo-timer-fill {
height: 100%;
background: linear-gradient(90deg, #FFD700, #ff6b35);
border-radius: 2px;
transition: width 0.1s linear;
}
@keyframes comboPulse {
0% { transform: scale(1.3); }
100% { transform: scale(1); }
}
/* Power-ups Bar */
.powerups-bar {
position: fixed;
bottom: 120px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 200;
}
.powerup-slot {
width: 50px;
height: 50px;
background: rgba(10, 15, 30, 0.9);
border: 2px solid rgba(0, 212, 255, 0.3);
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.powerup-slot.active {
border-color: #FFD700;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
animation: powerupGlow 1s ease-in-out infinite;
}
.powerup-slot .powerup-icon {
font-size: 24px;
}
.powerup-slot .powerup-timer {
position: absolute;
bottom: -8px;
font-size: 10px;
font-family: 'Orbitron', sans-serif;
background: rgba(0, 0, 0, 0.8);
padding: 2px 6px;
border-radius: 8px;
}
.powerup-slot.empty {
opacity: 0.5;
}
.powerup-slot.empty .powerup-icon {
opacity: 0.3;
}
@keyframes powerupGlow {
0%, 100% { box-shadow: 0 0 15px rgba(255, 215, 0, 0.5); }
50% { box-shadow: 0 0 25px rgba(255, 215, 0, 0.8); }
}
/* Streak Display */
.streak-display {
position: fixed;
top: 75px;
left: 24px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(10, 15, 30, 0.9);
border: 1px solid rgba(255, 107, 53, 0.5);
border-radius: 20px;
z-index: 100;
}
.streak-fire {
font-size: 20px;
animation: fireFlicker 0.5s ease-in-out infinite alternate;
}
.streak-count {
font-family: 'Orbitron', sans-serif;
font-size: 18px;
font-weight: 700;
color: #ff6b35;
}
.streak-label {
font-size: 10px;
letter-spacing: 1px;
opacity: 0.7;
}
.streak-bonus {
font-size: 11px;
color: #FFD700;
margin-left: 8px;
}
@keyframes fireFlicker {
0% { transform: scale(1); }
100% { transform: scale(1.1); }
}
/* Daily Reward Popup */
.daily-reward-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease-out;
}
.daily-popup-content {
background: linear-gradient(135deg, rgba(20, 30, 50, 0.98), rgba(10, 15, 30, 0.98));
border: 2px solid #FFD700;
border-radius: 20px;
padding: 30px 40px;
text-align: center;
max-width: 400px;
box-shadow: 0 0 50px rgba(255, 215, 0, 0.3);
animation: popupBounce 0.4s ease-out;
}
.daily-popup-header {
margin-bottom: 24px;
}
.daily-popup-header .fire-icon {
font-size: 40px;
display: block;
margin-bottom: 8px;
animation: fireFlicker 0.5s ease-in-out infinite alternate;
}
.daily-popup-header .streak-count {
font-family: 'Orbitron', sans-serif;
font-size: 28px;
font-weight: 900;
color: #FFD700;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
.daily-popup-reward {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.daily-popup-reward .reward-title {
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 8px;
}
.daily-popup-reward .reward-xp {
font-family: 'Orbitron', sans-serif;
font-size: 32px;
font-weight: 700;
color: #00d4ff;
margin-bottom: 12px;
}
.daily-popup-reward .reward-items {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.daily-popup-reward .reward-item {
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: 8px;
font-size: 14px;
}
.daily-popup-reward .reward-bonus {
margin-top: 12px;
font-size: 14px;
color: #FFD700;
}
.daily-claim-btn {
background: linear-gradient(135deg, #FFD700, #ff6b35);
color: #000;
font-family: 'Orbitron', sans-serif;
font-size: 16px;
font-weight: 700;
padding: 14px 40px;
border: none;
border-radius: 30px;
cursor: pointer;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 2px;
}
.daily-claim-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
@keyframes popupBounce {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
.daily-reward-popup.fade-out {
animation: fadeOut 0.5s ease-out forwards;
}
@keyframes fadeOut {
to { opacity: 0; }
}
/* Profile Panel */
.profile-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
max-width: 95vw;
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;
}
.profile-content {
padding: 20px;
}
.profile-preview {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
}
.profile-preview-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
border: 3px solid rgba(0, 212, 255, 0.5);
margin-bottom: 12px;
}
.profile-preview-name {
font-family: 'Orbitron', sans-serif;
font-size: 20px;
font-weight: 700;
}
.profile-preview-level {
font-size: 12px;
opacity: 0.7;
margin-top: 4px;
}
.profile-section {
margin-bottom: 20px;
}
.profile-section h4 {
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 12px;
}
.profile-section input[type="text"] {
width: 100%;
padding: 12px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 8px;
color: #fff;
font-size: 16px;
outline: none;
}
.profile-section input[type="text"]:focus {
border-color: #00d4ff;
}
.avatar-icons,
.avatar-colors {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.avatar-icon-btn,
.avatar-color-btn {
width: 44px;
height: 44px;
border-radius: 12px;
border: 2px solid transparent;
background: rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.avatar-icon-btn:hover,
.avatar-color-btn:hover {
border-color: rgba(0, 212, 255, 0.5);
transform: scale(1.1);
}
.avatar-icon-btn.selected,
.avatar-color-btn.selected {
border-color: #00d4ff;
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.profile-save-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #00d4ff, #0066ff);
color: #fff;
font-family: 'Orbitron', sans-serif;
font-size: 14px;
font-weight: 700;
border: none;
border-radius: 8px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 2px;
transition: all 0.2s ease;
}
.profile-save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.4);
}
/* Leaderboard Panel */
.leaderboard-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;
}
.leaderboard-content {
max-height: calc(80vh - 60px);
overflow-y: auto;
}
.leaderboard-tabs {
display: flex;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.2);
}
.leaderboard-tab {
flex: 1;
padding: 12px;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.leaderboard-tab:hover {
color: #fff;
background: rgba(255, 255, 255, 0.05);
}
.leaderboard-tab.active {
color: #00d4ff;
border-bottom: 2px solid #00d4ff;
}
.leaderboard-records {
padding: 20px;
}
.leaderboard-section {
margin-bottom: 20px;
}
.leaderboard-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #00d4ff;
}
.leaderboard-list {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
overflow: hidden;
}
.leaderboard-entry {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.leaderboard-entry:last-child {
border-bottom: none;
}
.leaderboard-entry.rank-1 { background: linear-gradient(90deg, rgba(255, 215, 0, 0.15), transparent); }
.leaderboard-entry.rank-2 { background: linear-gradient(90deg, rgba(192, 192, 192, 0.1), transparent); }
.leaderboard-entry.rank-3 { background: linear-gradient(90deg, rgba(205, 127, 50, 0.1), transparent); }
.leaderboard-entry.current-player {
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 8px;
}
.entry-rank {
font-size: 20px;
width: 40px;
text-align: center;
}
.entry-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-right: 12px;
}
.entry-info {
flex: 1;
}
.entry-name {
font-weight: 600;
margin-bottom: 2px;
}
.entry-value {
font-size: 12px;
opacity: 0.7;
}
.entry-badge {
background: rgba(0, 212, 255, 0.2);
color: #00d4ff;
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
}
.leaderboard-placeholder {
text-align: center;
padding: 30px;
opacity: 0.6;
}
.placeholder-icon {
font-size: 40px;
margin-bottom: 12px;
}
.personal-best {
text-align: center;
padding: 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
}
.personal-best-value {
font-family: 'Orbitron', sans-serif;
font-size: 28px;
font-weight: 700;
color: #FFD700;
margin-bottom: 4px;
}
.personal-best-label {
font-size: 12px;
opacity: 0.6;
}
.leaderboard-your-stats {
padding: 20px;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.your-stats-header {
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 12px;
}
.your-stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.your-stat-item {
text-align: center;
padding: 12px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
}
.your-stat-icon {
font-size: 20px;
margin-bottom: 4px;
}
.your-stat-value {
font-family: 'Orbitron', sans-serif;
font-size: 14px;
font-weight: 700;
color: #00d4ff;
}
.your-stat-label {
font-size: 10px;
opacity: 0.6;
margin-top: 2px;
}
/* Quick Travel Panel */
.quick-travel-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;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
z-index: 600;
overflow: hidden;
}
.quick-travel-content {
padding: 20px;
max-height: calc(80vh - 60px);
overflow-y: auto;
}
.quick-travel-cooldown {
background: rgba(255, 107, 53, 0.2);
border: 1px solid rgba(255, 107, 53, 0.5);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
}
.cooldown-text {
font-size: 12px;
text-align: center;
margin-bottom: 8px;
}
.cooldown-bar {
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
overflow: hidden;
}
.cooldown-fill {
height: 100%;
background: linear-gradient(90deg, #ff6b35, #FFD700);
transition: width 1s linear;
}
.quick-travel-sections {
display: flex;
flex-direction: column;
gap: 20px;
}
.quick-travel-section h4 {
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 12px;
}
.travel-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 180px;
overflow-y: auto;
}
.travel-btn {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #fff;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
width: 100%;
}
.travel-btn:hover:not(.disabled) {
background: rgba(0, 212, 255, 0.1);
border-color: rgba(0, 212, 255, 0.3);
transform: translateX(4px);
}
.travel-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.travel-icon {
font-size: 24px;
}
.travel-name {
flex: 1;
font-weight: 600;
}
.travel-coords {
font-size: 10px;
opacity: 0.5;
}
.travel-empty {
text-align: center;
padding: 20px;
opacity: 0.6;
font-size: 13px;
}
.travel-random-btn {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, rgba(255, 107, 53, 0.3), rgba(255, 215, 0, 0.3));
border: 1px solid rgba(255, 215, 0, 0.5);
border-radius: 12px;
color: #fff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.travel-random-btn:hover:not(.disabled) {
background: linear-gradient(135deg, rgba(255, 107, 53, 0.5), rgba(255, 215, 0, 0.5));
transform: scale(1.02);
}
.travel-random-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Travel Animation */
.travel-animation {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.travel-flash {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle, rgba(0, 212, 255, 0.8) 0%, rgba(0, 0, 0, 0.9) 70%);
animation: travelFlash 1s ease-out forwards;
}
.travel-text {
font-family: 'Orbitron', sans-serif;
font-size: 32px;
font-weight: 900;
color: #fff;
text-shadow: 0 0 30px rgba(0, 212, 255, 0.8);
animation: travelPulse 0.5s ease-in-out infinite;
}
@keyframes travelFlash {
0% { opacity: 0; }
20% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes travelPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.travel-animation.fade-out {
animation: fadeOut 0.5s ease-out forwards;
}
/* Screenshot Preview */
.screenshot-preview-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;
}
.screenshot-preview-content {
background: rgba(20, 30, 50, 0.98);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 16px;
padding: 20px;
text-align: center;
max-width: 90vw;
}
.screenshot-preview-content h3 {
font-family: 'Orbitron', sans-serif;
margin-bottom: 16px;
color: #00d4ff;
}
.screenshot-preview-image {
max-width: 100%;
max-height: 60vh;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.screenshot-preview-actions {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 20px;
}
.screenshot-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;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.screenshot-btn:hover {
background: rgba(0, 212, 255, 0.3);
transform: translateY(-2px);
}
.screenshot-btn.primary {
background: linear-gradient(135deg, #00d4ff, #0066ff);
border: none;
}
.screenshot-btn.close {
background: rgba(255, 100, 100, 0.2);
border-color: rgba(255, 100, 100, 0.5);
}
/* Collection Bonus Text */
.collection-bonus {
font-family: 'Orbitron', sans-serif;
font-size: 10px;
color: #FFD700;
animation: bonusPulse 0.5s ease-out;
}
@keyframes bonusPulse {
0% { transform: scale(1.3); }
100% { transform: scale(1); }
}
/* XP Popup (for particle effects) */
.xp-popup {
position: fixed;
font-family: 'Orbitron', sans-serif;
font-size: 18px;
font-weight: 700;
color: #FFD700;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.8);
pointer-events: none;
animation: xpFloat 1s ease-out forwards;
z-index: 501;
}
@keyframes xpFloat {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-50px) scale(1.2);
}
}
/* Responsive Adjustments for v5.0 */
@media (max-width: 768px) {
.combo-display {
right: 10px;
}
.combo-counter {
font-size: 32px;
}
.powerups-bar {
bottom: 100px;
}
.powerup-slot {
width: 40px;
height: 40px;
}
.daily-popup-content {
padding: 20px;
}
.profile-panel,
.leaderboard-panel,
.quick-travel-panel {
width: 95vw;
}
.your-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.screenshot-preview-actions {
flex-wrap: wrap;
}
}

200
src/js/comboSystem.js Normal file
View File

@@ -0,0 +1,200 @@
// Combo System for RoadWorld
// Rewards rapid item collection with bonus XP multipliers
export class ComboSystem {
constructor(gameEngine) {
this.gameEngine = gameEngine;
// Combo state
this.currentCombo = 0;
this.maxCombo = 0;
this.lastCollectionTime = 0;
this.comboTimeout = 5000; // 5 seconds to maintain combo
// Combo tiers
this.comboTiers = [
{ min: 0, name: '', multiplier: 1, color: '#fff' },
{ min: 3, name: 'COMBO!', multiplier: 1.25, color: '#00d4ff' },
{ min: 5, name: 'SUPER!', multiplier: 1.5, color: '#7b2ff7' },
{ min: 10, name: 'MEGA!', multiplier: 2, color: '#FF6B00' },
{ min: 15, name: 'ULTRA!', multiplier: 2.5, color: '#FF0066' },
{ min: 20, name: 'INSANE!', multiplier: 3, color: '#FFD700' },
{ min: 30, name: 'LEGENDARY!', multiplier: 4, color: '#FFD700' }
];
// Timer for combo decay
this.comboTimer = null;
this.comboElement = null;
this.init();
}
init() {
this.createComboDisplay();
}
createComboDisplay() {
this.comboElement = document.createElement('div');
this.comboElement.className = 'combo-display';
this.comboElement.innerHTML = `
<div class="combo-counter">
<span class="combo-number">0</span>
<span class="combo-label">COMBO</span>
</div>
<div class="combo-tier"></div>
<div class="combo-multiplier"></div>
<div class="combo-timer-bar">
<div class="combo-timer-fill"></div>
</div>
`;
this.comboElement.style.display = 'none';
document.body.appendChild(this.comboElement);
}
registerCollection(collectible) {
const now = Date.now();
// Check if combo should continue or reset
if (now - this.lastCollectionTime > this.comboTimeout) {
this.currentCombo = 0;
}
// Increment combo
this.currentCombo++;
this.lastCollectionTime = now;
// Update max combo
if (this.currentCombo > this.maxCombo) {
this.maxCombo = this.currentCombo;
}
// Get current tier
const tier = this.getCurrentTier();
// Update display
this.updateDisplay(tier);
// Reset combo timer
this.resetComboTimer();
// Return XP multiplier
return {
combo: this.currentCombo,
tier: tier,
multiplier: tier.multiplier
};
}
getCurrentTier() {
let tier = this.comboTiers[0];
for (const t of this.comboTiers) {
if (this.currentCombo >= t.min) {
tier = t;
}
}
return tier;
}
updateDisplay(tier) {
if (!this.comboElement) return;
const numberEl = this.comboElement.querySelector('.combo-number');
const tierEl = this.comboElement.querySelector('.combo-tier');
const multEl = this.comboElement.querySelector('.combo-multiplier');
numberEl.textContent = this.currentCombo;
numberEl.style.color = tier.color;
if (tier.name) {
tierEl.textContent = tier.name;
tierEl.style.color = tier.color;
this.comboElement.style.display = 'block';
this.comboElement.classList.add('active');
// Animate on tier change
if (this.currentCombo === tier.min) {
this.comboElement.classList.remove('tier-up');
void this.comboElement.offsetWidth; // Force reflow
this.comboElement.classList.add('tier-up');
}
}
multEl.textContent = tier.multiplier > 1 ? `x${tier.multiplier} XP` : '';
// Pulse animation on each collection
numberEl.classList.remove('pulse');
void numberEl.offsetWidth;
numberEl.classList.add('pulse');
}
resetComboTimer() {
if (this.comboTimer) {
clearTimeout(this.comboTimer);
clearInterval(this.timerInterval);
}
// Animate timer bar
const timerFill = this.comboElement.querySelector('.combo-timer-fill');
if (timerFill) {
timerFill.style.transition = 'none';
timerFill.style.width = '100%';
void timerFill.offsetWidth;
timerFill.style.transition = `width ${this.comboTimeout}ms linear`;
timerFill.style.width = '0%';
}
// Set timeout to end combo
this.comboTimer = setTimeout(() => {
this.endCombo();
}, this.comboTimeout);
}
endCombo() {
if (this.currentCombo >= 3) {
// Show combo end summary
this.showComboSummary();
}
this.currentCombo = 0;
this.comboElement.classList.remove('active');
setTimeout(() => {
if (this.currentCombo === 0) {
this.comboElement.style.display = 'none';
}
}, 500);
}
showComboSummary() {
const tier = this.getCurrentTier();
const summary = document.createElement('div');
summary.className = 'combo-summary';
summary.innerHTML = `
<div class="combo-summary-title">Combo Ended!</div>
<div class="combo-summary-count" style="color: ${tier.color}">${this.currentCombo}x</div>
<div class="combo-summary-tier">${tier.name || 'Nice!'}</div>
`;
document.body.appendChild(summary);
setTimeout(() => {
summary.classList.add('fade-out');
setTimeout(() => summary.remove(), 500);
}, 2000);
}
getComboInfo() {
return {
current: this.currentCombo,
max: this.maxCombo,
tier: this.getCurrentTier(),
multiplier: this.getCurrentTier().multiplier
};
}
// Get total XP with combo multiplier applied
calculateXP(baseXP) {
const multiplier = this.getCurrentTier().multiplier;
return Math.floor(baseXP * multiplier);
}
}

227
src/js/keyboardControls.js Normal file
View File

@@ -0,0 +1,227 @@
// Keyboard Controls for RoadWorld
// WASD movement with smooth camera following
export class KeyboardControls {
constructor(mapManager, gameEngine, playerAvatar) {
this.mapManager = mapManager;
this.gameEngine = gameEngine;
this.playerAvatar = playerAvatar;
this.isEnabled = false;
this.keys = new Set();
this.moveSpeed = 0.0005; // Base movement speed
this.sprintMultiplier = 2.5;
this.isSprinting = false;
// Movement state
this.moveInterval = null;
this.lastMoveTime = 0;
// Key bindings
this.bindings = {
forward: ['w', 'W', 'ArrowUp'],
backward: ['s', 'S', 'ArrowDown'],
left: ['a', 'A', 'ArrowLeft'],
right: ['d', 'D', 'ArrowRight'],
sprint: ['Shift'],
interact: ['e', 'E', ' '],
rotateLeft: ['q', 'Q'],
rotateRight: ['r', 'R']
};
}
init() {
this.setupEventListeners();
console.log('⌨️ Keyboard Controls initialized');
}
setupEventListeners() {
document.addEventListener('keydown', (e) => this.onKeyDown(e));
document.addEventListener('keyup', (e) => this.onKeyUp(e));
document.addEventListener('blur', () => this.clearKeys());
}
onKeyDown(e) {
if (!this.isEnabled) return;
// Ignore if typing in input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
const key = e.key;
// Check for sprint
if (this.bindings.sprint.includes(key)) {
this.isSprinting = true;
}
// Check for movement keys
if (this.isMovementKey(key)) {
e.preventDefault();
this.keys.add(key);
this.startMovement();
}
// Rotation
if (this.bindings.rotateLeft.includes(key)) {
e.preventDefault();
this.rotateBearing(-15);
}
if (this.bindings.rotateRight.includes(key)) {
e.preventDefault();
this.rotateBearing(15);
}
// Interact
if (this.bindings.interact.includes(key)) {
e.preventDefault();
this.interact();
}
}
onKeyUp(e) {
const key = e.key;
if (this.bindings.sprint.includes(key)) {
this.isSprinting = false;
}
this.keys.delete(key);
if (this.keys.size === 0) {
this.stopMovement();
}
}
clearKeys() {
this.keys.clear();
this.isSprinting = false;
this.stopMovement();
}
isMovementKey(key) {
return [...this.bindings.forward, ...this.bindings.backward,
...this.bindings.left, ...this.bindings.right].includes(key);
}
startMovement() {
if (this.moveInterval) return;
this.moveInterval = setInterval(() => {
this.processMovement();
}, 16); // ~60fps
}
stopMovement() {
if (this.moveInterval) {
clearInterval(this.moveInterval);
this.moveInterval = null;
}
}
processMovement() {
if (!this.gameEngine.player) return;
const bearing = this.mapManager.getBearing() * Math.PI / 180;
let dx = 0;
let dy = 0;
// Calculate movement direction
for (const key of this.keys) {
if (this.bindings.forward.includes(key)) {
dx += Math.sin(bearing);
dy += Math.cos(bearing);
}
if (this.bindings.backward.includes(key)) {
dx -= Math.sin(bearing);
dy -= Math.cos(bearing);
}
if (this.bindings.left.includes(key)) {
dx -= Math.cos(bearing);
dy += Math.sin(bearing);
}
if (this.bindings.right.includes(key)) {
dx += Math.cos(bearing);
dy -= Math.sin(bearing);
}
}
// Normalize diagonal movement
if (dx !== 0 && dy !== 0) {
const length = Math.sqrt(dx * dx + dy * dy);
dx /= length;
dy /= length;
}
if (dx === 0 && dy === 0) return;
// Apply speed and sprint
const speed = this.moveSpeed * (this.isSprinting ? this.sprintMultiplier : 1);
const zoom = this.mapManager.getZoom();
const zoomFactor = Math.pow(2, 20 - zoom) / 1000; // Adjust speed based on zoom
const currentPos = this.gameEngine.player.position;
const newPos = [
currentPos[0] + dx * speed * zoomFactor,
currentPos[1] + dy * speed * zoomFactor
];
// Move player
const distance = this.gameEngine.movePlayer(newPos);
// Update avatar
if (this.playerAvatar) {
this.playerAvatar.updatePosition(newPos);
}
// Pan map to follow player
this.mapManager.map.panTo(newPos, { duration: 50 });
// Award XP for movement
const now = Date.now();
if (now - this.lastMoveTime > 1000 && distance > 5) {
const xp = Math.floor(distance / 10);
if (xp > 0) {
this.gameEngine.addXP(xp, 'keyboard movement');
}
this.lastMoveTime = now;
}
}
rotateBearing(degrees) {
const currentBearing = this.mapManager.getBearing();
this.mapManager.easeTo({
bearing: currentBearing + degrees,
duration: 200
});
}
interact() {
// Trigger collection check or interaction
if (this.gameEngine) {
this.gameEngine.checkCollectibles();
}
}
enable() {
this.isEnabled = true;
console.log('⌨️ Keyboard controls enabled (WASD + Shift to sprint)');
}
disable() {
this.isEnabled = false;
this.clearKeys();
}
toggle() {
if (this.isEnabled) {
this.disable();
} else {
this.enable();
}
return this.isEnabled;
}
setSpeed(speed) {
this.moveSpeed = speed;
}
}

256
src/js/leaderboardPanel.js Normal file
View File

@@ -0,0 +1,256 @@
// Leaderboard Panel for RoadWorld
// Displays player rankings and records
export class LeaderboardPanel {
constructor(gameEngine, storageManager) {
this.gameEngine = gameEngine;
this.storageManager = storageManager;
this.panelElement = null;
this.isVisible = false;
// Leaderboard categories
this.categories = {
xp: { name: 'Experience', icon: '⭐', format: (v) => `${v.toLocaleString()} XP` },
distance: { name: 'Distance', icon: '🚶', format: (v) => `${(v/1000).toFixed(2)} km` },
items: { name: 'Items Collected', icon: '✨', format: (v) => v.toLocaleString() },
level: { name: 'Level', icon: '🎖️', format: (v) => `Level ${v}` },
streak: { name: 'Login Streak', icon: '🔥', format: (v) => `${v} days` },
combo: { name: 'Max Combo', icon: '💥', format: (v) => `${v}x` }
};
}
init() {
this.createPanel();
this.loadRecords();
console.log('🏆 Leaderboard Panel initialized');
}
createPanel() {
this.panelElement = document.createElement('div');
this.panelElement.className = 'leaderboard-panel ui-overlay';
this.panelElement.id = 'leaderboard-panel';
this.panelElement.style.display = 'none';
this.panelElement.innerHTML = `
<div class="panel-header">
<span>🏆 Leaderboards</span>
<button class="panel-close" id="leaderboard-close">✕</button>
</div>
<div class="panel-content leaderboard-content">
<div class="leaderboard-tabs">
<button class="leaderboard-tab active" data-category="xp">⭐ XP</button>
<button class="leaderboard-tab" data-category="distance">🚶 Distance</button>
<button class="leaderboard-tab" data-category="items">✨ Items</button>
<button class="leaderboard-tab" data-category="level">🎖️ Level</button>
</div>
<div class="leaderboard-records" id="leaderboard-records"></div>
<div class="leaderboard-your-stats">
<div class="your-stats-header">Your Records</div>
<div class="your-stats-grid" id="your-stats-grid"></div>
</div>
</div>
`;
document.body.appendChild(this.panelElement);
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('leaderboard-close').addEventListener('click', () => {
this.hide();
});
this.panelElement.querySelectorAll('.leaderboard-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const category = e.target.dataset.category;
this.switchCategory(category);
});
});
}
loadRecords() {
// Load personal records
this.records = this.storageManager.data.records || {
highestXP: 0,
furthestDistance: 0,
mostItems: 0,
highestLevel: 0,
longestStreak: 0,
maxCombo: 0,
fastestLevel: Infinity,
sessions: []
};
}
saveRecords() {
this.storageManager.data.records = this.records;
this.storageManager.save();
}
updateRecord(category, value) {
const recordKey = this.getRecordKey(category);
if (!recordKey) return;
const currentRecord = this.records[recordKey] || 0;
if (category === 'fastestLevel') {
if (value < currentRecord) {
this.records[recordKey] = value;
this.saveRecords();
return { isNewRecord: true, oldRecord: currentRecord, newRecord: value };
}
} else {
if (value > currentRecord) {
this.records[recordKey] = value;
this.saveRecords();
return { isNewRecord: true, oldRecord: currentRecord, newRecord: value };
}
}
return { isNewRecord: false };
}
getRecordKey(category) {
const keys = {
xp: 'highestXP',
distance: 'furthestDistance',
items: 'mostItems',
level: 'highestLevel',
streak: 'longestStreak',
combo: 'maxCombo'
};
return keys[category];
}
switchCategory(category) {
// Update active tab
this.panelElement.querySelectorAll('.leaderboard-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.category === category);
});
// Show records for category
this.renderCategory(category);
}
renderCategory(category) {
const recordsEl = document.getElementById('leaderboard-records');
const cat = this.categories[category];
// For now, show personal best (could be expanded with actual multiplayer)
const player = this.gameEngine.player;
let value;
switch (category) {
case 'xp':
value = player.xp + (player.level - 1) * player.xpToNextLevel;
break;
case 'distance':
value = player.stats.distanceTraveled;
break;
case 'items':
value = player.stats.itemsCollected;
break;
case 'level':
value = player.level;
break;
}
recordsEl.innerHTML = `
<div class="leaderboard-section">
<div class="leaderboard-title">${cat.icon} ${cat.name} Rankings</div>
<div class="leaderboard-list">
<div class="leaderboard-entry rank-1 current-player">
<div class="entry-rank">🥇</div>
<div class="entry-avatar" style="background: ${player.avatar.color}">🧑‍🚀</div>
<div class="entry-info">
<div class="entry-name">${player.username}</div>
<div class="entry-value">${cat.format(value)}</div>
</div>
<div class="entry-badge">You!</div>
</div>
<div class="leaderboard-placeholder">
<div class="placeholder-icon">🌐</div>
<div class="placeholder-text">
Global leaderboards coming soon!<br>
<small>Connect with players worldwide</small>
</div>
</div>
</div>
</div>
<div class="leaderboard-section">
<div class="leaderboard-title">📊 Personal Best</div>
<div class="personal-best">
<div class="personal-best-value">${cat.format(this.records[this.getRecordKey(category)] || value)}</div>
<div class="personal-best-label">Your all-time record</div>
</div>
</div>
`;
this.renderYourStats();
}
renderYourStats() {
const player = this.gameEngine.player;
const statsGrid = document.getElementById('your-stats-grid');
const stats = [
{ icon: '⭐', label: 'Total XP', value: player.xp },
{ icon: '🎖️', label: 'Level', value: player.level },
{ icon: '🚶', label: 'Distance', value: `${(player.stats.distanceTraveled/1000).toFixed(2)} km` },
{ icon: '✨', label: 'Items', value: player.stats.itemsCollected },
{ icon: '🔥', label: 'Streak', value: `${this.records.longestStreak || 0} days` },
{ icon: '💥', label: 'Max Combo', value: `${this.records.maxCombo || 0}x` }
];
statsGrid.innerHTML = stats.map(stat => `
<div class="your-stat-item">
<div class="your-stat-icon">${stat.icon}</div>
<div class="your-stat-value">${stat.value}</div>
<div class="your-stat-label">${stat.label}</div>
</div>
`).join('');
}
show() {
this.isVisible = true;
this.panelElement.style.display = 'block';
this.switchCategory('xp');
}
hide() {
this.isVisible = false;
this.panelElement.style.display = 'none';
}
toggle() {
if (this.isVisible) {
this.hide();
} else {
this.show();
}
}
// Check for new records after game actions
checkAllRecords() {
const player = this.gameEngine.player;
const checks = [
{ cat: 'xp', value: player.xp },
{ cat: 'distance', value: player.stats.distanceTraveled },
{ cat: 'items', value: player.stats.itemsCollected },
{ cat: 'level', value: player.level }
];
const newRecords = [];
checks.forEach(check => {
const result = this.updateRecord(check.cat, check.value);
if (result.isNewRecord) {
newRecords.push({ category: check.cat, ...result });
}
});
return newRecords;
}
}

View File

@@ -10,7 +10,7 @@ import { GameEngine } from './gameEngine.js';
import { PlayerAvatar } from './playerAvatar.js'; import { PlayerAvatar } from './playerAvatar.js';
import { CollectiblesRenderer } from './collectiblesRenderer.js'; import { CollectiblesRenderer } from './collectiblesRenderer.js';
// New enhanced features // Enhanced features (v4.0)
import { AchievementsManager } from './achievementsManager.js'; import { AchievementsManager } from './achievementsManager.js';
import { MissionsManager } from './missionsManager.js'; import { MissionsManager } from './missionsManager.js';
import { WeatherEffects } from './weatherEffects.js'; import { WeatherEffects } from './weatherEffects.js';
@@ -18,6 +18,17 @@ import { SoundManager } from './soundManager.js';
import { Minimap } from './minimap.js'; import { Minimap } from './minimap.js';
import { StatisticsPanel } from './statisticsPanel.js'; import { StatisticsPanel } from './statisticsPanel.js';
// Next-level features (v5.0)
import { KeyboardControls } from './keyboardControls.js';
import { StreakManager } from './streakManager.js';
import { ComboSystem } from './comboSystem.js';
import { PowerUpsManager } from './powerUpsManager.js';
import { ScreenshotCapture } from './screenshotCapture.js';
import { LeaderboardPanel } from './leaderboardPanel.js';
import { ProfileManager } from './profileManager.js';
import { ParticleEffects } from './particleEffects.js';
import { QuickTravel } from './quickTravel.js';
class RoadWorldApp { class RoadWorldApp {
constructor() { constructor() {
this.mapManager = null; this.mapManager = null;
@@ -42,6 +53,17 @@ class RoadWorldApp {
this.soundManager = null; this.soundManager = null;
this.minimap = null; this.minimap = null;
this.statisticsPanel = null; this.statisticsPanel = null;
// Next-level features (v5.0)
this.keyboardControls = null;
this.streakManager = null;
this.comboSystem = null;
this.powerUpsManager = null;
this.screenshotCapture = null;
this.leaderboardPanel = null;
this.profileManager = null;
this.particleEffects = null;
this.quickTravel = null;
} }
async init() { async init() {
@@ -99,7 +121,13 @@ class RoadWorldApp {
// Initialize enhanced features (v4.0) // Initialize enhanced features (v4.0)
this.initEnhancedFeatures(); this.initEnhancedFeatures();
console.log('RoadWorld v4.0 initialized with enhanced features'); // Initialize next-level features (v5.0)
this.initNextLevelFeatures();
// Make app globally accessible
window.app = this;
console.log('RoadWorld v5.0 initialized - NEXT LEVEL!');
} }
initEnhancedFeatures() { initEnhancedFeatures() {
@@ -171,6 +199,153 @@ class RoadWorldApp {
} }
} }
initNextLevelFeatures() {
// Particle Effects (initialize first for visual feedback)
this.particleEffects = new ParticleEffects();
this.particleEffects.init();
// Combo System
this.comboSystem = new ComboSystem(this.gameEngine);
// Power-ups Manager
this.powerUpsManager = new PowerUpsManager(this.gameEngine, this.storageManager);
// Streak Manager
this.streakManager = new StreakManager(this.gameEngine, this.storageManager);
// Screenshot Capture
this.screenshotCapture = new ScreenshotCapture(this.mapManager);
// Leaderboard Panel
this.leaderboardPanel = new LeaderboardPanel(this.gameEngine, this.storageManager);
this.leaderboardPanel.init();
// Profile Manager
this.profileManager = new ProfileManager(this.gameEngine, this.storageManager);
this.profileManager.init();
// Quick Travel
this.quickTravel = new QuickTravel(this.mapManager, this.gameEngine, this.storageManager);
this.quickTravel.init();
// Keyboard Controls (initialize last)
this.keyboardControls = new KeyboardControls(this.mapManager, this.gameEngine, this.playerAvatar);
this.keyboardControls.init();
// Setup v5.0 controls
this.setupNextLevelControls();
// Check for daily login
this.checkDailyLogin();
}
setupNextLevelControls() {
// Screenshot button
const screenshotBtn = document.getElementById('btn-screenshot');
if (screenshotBtn) {
screenshotBtn.addEventListener('click', () => {
this.screenshotCapture.preview();
});
}
// Leaderboard button
const leaderboardBtn = document.getElementById('btn-leaderboard');
if (leaderboardBtn) {
leaderboardBtn.addEventListener('click', () => {
this.leaderboardPanel.toggle();
});
}
// Profile button
const profileBtn = document.getElementById('btn-profile');
if (profileBtn) {
profileBtn.addEventListener('click', () => {
this.profileManager.toggle();
});
}
// Quick Travel button
const travelBtn = document.getElementById('btn-travel');
if (travelBtn) {
travelBtn.addEventListener('click', () => {
this.quickTravel.toggle();
});
}
// Keyboard toggle (activates with game mode)
const keyboardBtn = document.getElementById('btn-keyboard');
if (keyboardBtn) {
keyboardBtn.addEventListener('click', () => {
const isEnabled = this.keyboardControls.toggle();
keyboardBtn.classList.toggle('active', isEnabled);
this.showNotification(isEnabled ? 'WASD controls enabled' : 'WASD controls disabled');
});
}
}
checkDailyLogin() {
const result = this.streakManager.checkLogin();
if (result.isNewDay && !result.streakContinues && result.streak > 1) {
// Streak was broken - show message
setTimeout(() => {
this.showNotification('Streak reset! Start a new one today!');
}, 2000);
} else if (result.isNewDay) {
// Show daily reward popup
setTimeout(() => {
this.showDailyRewardPopup(result);
}, 1500);
}
}
showDailyRewardPopup(result) {
const reward = result.reward;
if (!reward) return;
const popup = document.createElement('div');
popup.className = 'daily-reward-popup';
popup.innerHTML = `
<div class="daily-popup-content">
<div class="daily-popup-header">
<span class="fire-icon">🔥</span>
<span class="streak-count">${result.streak} Day Streak!</span>
</div>
<div class="daily-popup-reward">
<div class="reward-title">Today's Reward</div>
<div class="reward-xp">+${reward.xp} XP</div>
<div class="reward-items">
${Object.entries(reward.items || {}).map(([item, count]) =>
`<span class="reward-item">${this.getItemIcon(item)} x${count}</span>`
).join('')}
</div>
${reward.bonus ? `<div class="reward-bonus">🌟 ${reward.bonus}</div>` : ''}
</div>
<button class="daily-claim-btn" id="claim-daily">Claim Reward!</button>
</div>
`;
document.body.appendChild(popup);
document.getElementById('claim-daily').addEventListener('click', () => {
this.streakManager.claimDailyReward();
popup.classList.add('fade-out');
setTimeout(() => popup.remove(), 500);
if (this.soundManager) {
this.soundManager.playAchievement();
}
if (this.particleEffects) {
this.particleEffects.levelUpBurst(window.innerWidth / 2, window.innerHeight / 2);
}
});
}
getItemIcon(item) {
const icons = { stars: '⭐', gems: '💎', trophies: '🏆', keys: '🗝️' };
return icons[item] || '✨';
}
setupGameToggle() { setupGameToggle() {
const toggle = document.getElementById('game-toggle'); const toggle = document.getElementById('game-toggle');
@@ -215,6 +390,11 @@ class RoadWorldApp {
this.onCollectItem(collectible); this.onCollectItem(collectible);
}; };
// Enable keyboard controls
if (this.keyboardControls) {
this.keyboardControls.enable();
}
// Initial HUD update // Initial HUD update
this.updateGameHUD(); this.updateGameHUD();
@@ -226,7 +406,7 @@ class RoadWorldApp {
this.soundManager.playAmbient('explore'); this.soundManager.playAmbient('explore');
} }
this.showNotification('🎮 Game Mode Activated! Click to move your avatar.'); this.showNotification('🎮 Game Mode Activated! Use WASD or click to move.');
} }
deactivateGameMode() { deactivateGameMode() {
@@ -241,6 +421,11 @@ class RoadWorldApp {
// Clear collectibles // Clear collectibles
this.collectiblesRenderer.clearAll(); this.collectiblesRenderer.clearAll();
// Disable keyboard controls
if (this.keyboardControls) {
this.keyboardControls.disable();
}
// Remove map click handler (would need to track the handler) // Remove map click handler (would need to track the handler)
// For now, game click will just be ignored when not active // For now, game click will just be ignored when not active
@@ -296,35 +481,95 @@ class RoadWorldApp {
} }
onCollectItem(collectible) { onCollectItem(collectible) {
// Register combo and get multiplier
let xpMultiplier = 1;
if (this.comboSystem) {
const comboResult = this.comboSystem.registerCollection(collectible);
xpMultiplier = comboResult.multiplier;
}
// Apply power-up XP multiplier
if (this.powerUpsManager) {
xpMultiplier *= this.powerUpsManager.getXPMultiplier();
}
// Apply streak bonus
if (this.streakManager) {
xpMultiplier *= this.streakManager.getXPMultiplier();
}
// Calculate final XP
const finalXP = Math.floor(collectible.xp * xpMultiplier);
// Play collection sound // Play collection sound
if (this.soundManager) { if (this.soundManager) {
this.soundManager.playCollect(collectible.rarity); this.soundManager.playCollect(collectible.rarity);
} }
// Show particle effects
if (this.particleEffects) {
const screenPos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
this.particleEffects.collectionBurst(screenPos.x, screenPos.y, collectible.rarity);
if (xpMultiplier > 1) {
this.particleEffects.xpPopup(screenPos.x, screenPos.y - 50, finalXP);
}
}
// Update missions // Update missions
if (this.missionsManager) { if (this.missionsManager) {
this.missionsManager.updateProgress('item', 1); this.missionsManager.updateProgress('item', 1);
this.missionsManager.updateProgress(collectible.type, 1); this.missionsManager.updateProgress(collectible.type, 1);
} }
// Show collection notification // Show collection notification (with bonus info)
this.showCollectionNotification(collectible); this.showCollectionNotification(collectible, xpMultiplier);
// Check achievements // Check achievements
this.checkAchievements(); this.checkAchievements();
// Check for power-up drops (rare chance)
this.checkPowerUpDrop(collectible);
// Update HUD // Update HUD
this.updateGameHUD(); this.updateGameHUD();
} }
showCollectionNotification(collectible) { checkPowerUpDrop(collectible) {
if (!this.powerUpsManager) return;
// Higher rarity = higher drop chance
const dropChance = collectible.rarity === 'legendary' ? 0.5 :
collectible.rarity === 'epic' ? 0.2 :
collectible.rarity === 'rare' ? 0.05 : 0.01;
if (Math.random() < dropChance) {
const powerUp = this.powerUpsManager.grantRandomPowerUp();
if (powerUp) {
this.showNotification(` Power-up: ${powerUp.name}!`);
if (this.soundManager) {
this.soundManager.playAchievement();
}
}
}
}
showCollectionNotification(collectible, xpMultiplier = 1) {
const finalXP = Math.floor(collectible.xp * xpMultiplier);
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.className = `collection-notification ${collectible.rarity}`; notification.className = `collection-notification ${collectible.rarity}`;
let bonusText = '';
if (xpMultiplier > 1) {
bonusText = `<div class="collection-bonus">x${xpMultiplier.toFixed(1)} BONUS!</div>`;
}
notification.innerHTML = ` notification.innerHTML = `
<div class="collection-icon">${collectible.icon}</div> <div class="collection-icon">${collectible.icon}</div>
<div class="collection-info"> <div class="collection-info">
<div class="collection-name">${collectible.type.charAt(0).toUpperCase() + collectible.type.slice(1)}</div> <div class="collection-name">${collectible.type.charAt(0).toUpperCase() + collectible.type.slice(1)}</div>
<div class="collection-xp">+${collectible.xp} XP</div> <div class="collection-xp">+${finalXP} XP${xpMultiplier > 1 ? ' 🔥' : ''}</div>
${bonusText}
</div> </div>
`; `;
document.body.appendChild(notification); document.body.appendChild(notification);

339
src/js/particleEffects.js Normal file
View File

@@ -0,0 +1,339 @@
// Particle Effects System for RoadWorld
// Creates visual effects for collections, level ups, etc.
export class ParticleEffects {
constructor() {
this.canvas = null;
this.ctx = null;
this.particles = [];
this.isRunning = false;
}
init() {
this.createCanvas();
this.startLoop();
console.log('✨ Particle Effects initialized');
}
createCanvas() {
this.canvas = document.createElement('canvas');
this.canvas.className = 'particle-canvas';
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
document.body.appendChild(this.canvas);
this.ctx = this.canvas.getContext('2d');
window.addEventListener('resize', () => {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
});
}
startLoop() {
this.isRunning = true;
this.animate();
}
animate() {
if (!this.isRunning) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Update and draw particles
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
// Update
particle.x += particle.vx;
particle.y += particle.vy;
particle.vy += particle.gravity || 0;
particle.life -= particle.decay;
particle.rotation += particle.rotationSpeed || 0;
if (particle.life <= 0) {
this.particles.splice(i, 1);
continue;
}
// Draw
this.ctx.save();
this.ctx.globalAlpha = particle.life;
this.ctx.translate(particle.x, particle.y);
this.ctx.rotate(particle.rotation);
if (particle.type === 'sparkle') {
this.drawSparkle(particle);
} else if (particle.type === 'star') {
this.drawStar(particle);
} else if (particle.type === 'confetti') {
this.drawConfetti(particle);
} else if (particle.type === 'ring') {
this.drawRing(particle);
} else if (particle.type === 'text') {
this.drawText(particle);
} else {
this.drawCircle(particle);
}
this.ctx.restore();
}
requestAnimationFrame(() => this.animate());
}
drawSparkle(p) {
const size = p.size * p.life;
this.ctx.fillStyle = p.color;
// 4-pointed star shape
this.ctx.beginPath();
for (let i = 0; i < 4; i++) {
const angle = (i * Math.PI) / 2;
const innerSize = size * 0.3;
this.ctx.lineTo(Math.cos(angle) * size, Math.sin(angle) * size);
this.ctx.lineTo(Math.cos(angle + Math.PI/4) * innerSize, Math.sin(angle + Math.PI/4) * innerSize);
}
this.ctx.closePath();
this.ctx.fill();
}
drawStar(p) {
const size = p.size * p.life;
this.ctx.font = `${size}px sans-serif`;
this.ctx.fillText('⭐', -size/2, size/2);
}
drawConfetti(p) {
this.ctx.fillStyle = p.color;
this.ctx.fillRect(-p.size/2, -p.size/4, p.size, p.size/2);
}
drawRing(p) {
const size = p.size * (2 - p.life);
this.ctx.strokeStyle = p.color;
this.ctx.lineWidth = 3 * p.life;
this.ctx.beginPath();
this.ctx.arc(0, 0, size, 0, Math.PI * 2);
this.ctx.stroke();
}
drawCircle(p) {
this.ctx.fillStyle = p.color;
this.ctx.beginPath();
this.ctx.arc(0, 0, p.size * p.life, 0, Math.PI * 2);
this.ctx.fill();
}
drawText(p) {
this.ctx.font = `bold ${p.size}px Orbitron, sans-serif`;
this.ctx.fillStyle = p.color;
this.ctx.textAlign = 'center';
this.ctx.fillText(p.text, 0, 0);
}
// Effect: Collection burst
collectionBurst(x, y, rarity = 'common') {
const colors = {
common: ['#fff', '#ddd', '#aaa'],
rare: ['#00d4ff', '#66e0ff', '#99ebff'],
epic: ['#7b2ff7', '#9955ff', '#bb88ff'],
legendary: ['#FFD700', '#FFA500', '#FFFF00']
};
const particleColors = colors[rarity] || colors.common;
const count = rarity === 'legendary' ? 30 : rarity === 'epic' ? 20 : 15;
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count + Math.random() * 0.5;
const speed = 3 + Math.random() * 4;
this.particles.push({
type: 'sparkle',
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: 8 + Math.random() * 6,
color: particleColors[Math.floor(Math.random() * particleColors.length)],
life: 1,
decay: 0.02 + Math.random() * 0.01,
gravity: 0.05,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.2
});
}
// Add ring effect for rare+
if (rarity !== 'common') {
this.particles.push({
type: 'ring',
x, y,
vx: 0, vy: 0,
size: 20,
color: particleColors[0],
life: 1,
decay: 0.03
});
}
}
// Effect: Level up celebration
levelUpBurst(x, y) {
// Confetti explosion
for (let i = 0; i < 50; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 5 + Math.random() * 8;
this.particles.push({
type: 'confetti',
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 5,
size: 8 + Math.random() * 4,
color: ['#FFD700', '#FF6B00', '#00d4ff', '#7b2ff7', '#FF0066'][Math.floor(Math.random() * 5)],
life: 1,
decay: 0.01,
gravity: 0.15,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.3
});
}
// Rising text
this.particles.push({
type: 'text',
text: 'LEVEL UP!',
x, y,
vx: 0,
vy: -2,
size: 32,
color: '#FFD700',
life: 1,
decay: 0.01,
rotation: 0
});
// Multiple rings
for (let i = 0; i < 3; i++) {
setTimeout(() => {
this.particles.push({
type: 'ring',
x, y,
vx: 0, vy: 0,
size: 30,
color: '#FFD700',
life: 1,
decay: 0.025
});
}, i * 100);
}
}
// Effect: Combo milestone
comboBurst(x, y, comboCount) {
const colors = comboCount >= 20 ? ['#FFD700', '#FFA500'] :
comboCount >= 10 ? ['#FF6B00', '#FF9900'] :
comboCount >= 5 ? ['#7b2ff7', '#9955ff'] :
['#00d4ff', '#66e0ff'];
for (let i = 0; i < comboCount; i++) {
setTimeout(() => {
const angle = Math.random() * Math.PI * 2;
this.particles.push({
type: 'sparkle',
x: x + (Math.random() - 0.5) * 50,
y: y + (Math.random() - 0.5) * 50,
vx: Math.cos(angle) * 2,
vy: Math.sin(angle) * 2 - 1,
size: 10,
color: colors[Math.floor(Math.random() * colors.length)],
life: 1,
decay: 0.03,
rotation: 0,
rotationSpeed: 0.1
});
}, i * 20);
}
// Combo text
this.particles.push({
type: 'text',
text: `${comboCount}x`,
x, y: y - 30,
vx: 0,
vy: -1.5,
size: 24 + Math.min(comboCount, 20),
color: colors[0],
life: 1,
decay: 0.015,
rotation: 0
});
}
// Effect: Achievement unlock
achievementBurst(x, y) {
// Golden particles
for (let i = 0; i < 40; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 4 + Math.random() * 6;
this.particles.push({
type: 'sparkle',
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: 10 + Math.random() * 8,
color: ['#FFD700', '#FFA500', '#FFFF00'][Math.floor(Math.random() * 3)],
life: 1,
decay: 0.015,
gravity: 0.02,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.15
});
}
// Trophy icon rising
this.particles.push({
type: 'text',
text: '🏆',
x, y,
vx: 0,
vy: -2,
size: 48,
color: '#FFD700',
life: 1,
decay: 0.008,
rotation: 0
});
}
// Effect: XP gain popup
xpPopup(x, y, amount) {
this.particles.push({
type: 'text',
text: `+${amount} XP`,
x: x + (Math.random() - 0.5) * 30,
y,
vx: (Math.random() - 0.5) * 0.5,
vy: -2,
size: 16,
color: '#00d4ff',
life: 1,
decay: 0.02,
rotation: 0
});
}
// Screen position from map coordinates
getScreenPosition(map, lngLat) {
const point = map.project(lngLat);
return { x: point.x, y: point.y };
}
destroy() {
this.isRunning = false;
if (this.canvas) {
this.canvas.remove();
}
}
}

268
src/js/powerUpsManager.js Normal file
View File

@@ -0,0 +1,268 @@
// Power-ups System for RoadWorld
// Temporary buffs that enhance gameplay
export class PowerUpsManager {
constructor(gameEngine, storageManager) {
this.gameEngine = gameEngine;
this.storageManager = storageManager;
// Active power-ups
this.activePowerUps = new Map();
// Power-up definitions
this.powerUpTypes = this.definePowerUps();
// UI element
this.powerUpBar = null;
this.init();
}
definePowerUps() {
return {
xp_boost: {
id: 'xp_boost',
name: 'XP Boost',
description: 'Double XP gain',
icon: '⚡',
color: '#FFD700',
duration: 300000, // 5 minutes
effect: { type: 'xp_multiplier', value: 2 }
},
magnet: {
id: 'magnet',
name: 'Item Magnet',
description: 'Increased collection radius',
icon: '🧲',
color: '#FF6B00',
duration: 180000, // 3 minutes
effect: { type: 'collection_radius', value: 3 }
},
lucky: {
id: 'lucky',
name: 'Lucky Star',
description: 'Better item rarity chances',
icon: '🍀',
color: '#00FF88',
duration: 240000, // 4 minutes
effect: { type: 'rarity_boost', value: 2 }
},
speed: {
id: 'speed',
name: 'Speed Boost',
description: 'Move faster',
icon: '💨',
color: '#00d4ff',
duration: 120000, // 2 minutes
effect: { type: 'speed_multiplier', value: 2 }
},
shield: {
id: 'shield',
name: 'Combo Shield',
description: 'Combo timer paused',
icon: '🛡️',
color: '#7b2ff7',
duration: 60000, // 1 minute
effect: { type: 'combo_freeze', value: true }
},
golden: {
id: 'golden',
name: 'Golden Hour',
description: 'All effects combined!',
icon: '👑',
color: '#FFD700',
duration: 60000, // 1 minute
effect: { type: 'all_boosts', value: true },
legendary: true
}
};
}
init() {
this.createPowerUpBar();
this.loadActivePowerUps();
this.startUpdateLoop();
}
createPowerUpBar() {
this.powerUpBar = document.createElement('div');
this.powerUpBar.className = 'power-up-bar';
this.powerUpBar.innerHTML = `<div class="power-up-list"></div>`;
document.body.appendChild(this.powerUpBar);
}
loadActivePowerUps() {
const saved = this.storageManager.data.activePowerUps || [];
const now = Date.now();
saved.forEach(pu => {
if (pu.expiresAt > now) {
this.activePowerUps.set(pu.id, pu);
}
});
this.updateUI();
}
savePowerUps() {
this.storageManager.data.activePowerUps = Array.from(this.activePowerUps.values());
this.storageManager.save();
}
activatePowerUp(typeId) {
const type = this.powerUpTypes[typeId];
if (!type) return null;
const now = Date.now();
// Check if already active - extend duration
if (this.activePowerUps.has(typeId)) {
const existing = this.activePowerUps.get(typeId);
existing.expiresAt += type.duration;
} else {
// Create new power-up
const powerUp = {
id: typeId,
...type,
activatedAt: now,
expiresAt: now + type.duration
};
this.activePowerUps.set(typeId, powerUp);
}
this.savePowerUps();
this.updateUI();
console.log(`⚡ Power-up activated: ${type.name}`);
return this.activePowerUps.get(typeId);
}
deactivatePowerUp(typeId) {
this.activePowerUps.delete(typeId);
this.savePowerUps();
this.updateUI();
}
isActive(typeId) {
if (!this.activePowerUps.has(typeId)) return false;
const pu = this.activePowerUps.get(typeId);
if (Date.now() > pu.expiresAt) {
this.deactivatePowerUp(typeId);
return false;
}
return true;
}
getEffectValue(effectType, defaultValue = 1) {
for (const pu of this.activePowerUps.values()) {
if (Date.now() > pu.expiresAt) continue;
if (pu.effect.type === effectType) {
return pu.effect.value;
}
// Golden hour gives all effects
if (pu.effect.type === 'all_boosts') {
if (effectType === 'xp_multiplier') return 2;
if (effectType === 'collection_radius') return 3;
if (effectType === 'rarity_boost') return 2;
if (effectType === 'speed_multiplier') return 1.5;
}
}
return defaultValue;
}
getXPMultiplier() {
return this.getEffectValue('xp_multiplier', 1);
}
getCollectionRadius() {
return this.getEffectValue('collection_radius', 1);
}
getRarityBoost() {
return this.getEffectValue('rarity_boost', 1);
}
getSpeedMultiplier() {
return this.getEffectValue('speed_multiplier', 1);
}
isComboFrozen() {
return this.isActive('shield') || this.isActive('golden');
}
startUpdateLoop() {
setInterval(() => {
this.checkExpired();
this.updateUI();
}, 1000);
}
checkExpired() {
const now = Date.now();
for (const [id, pu] of this.activePowerUps) {
if (now > pu.expiresAt) {
this.deactivatePowerUp(id);
this.showExpiredNotification(pu);
}
}
}
showExpiredNotification(powerUp) {
const notification = document.createElement('div');
notification.className = 'notification power-up-expired';
notification.innerHTML = `${powerUp.icon} ${powerUp.name} expired!`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideIn 0.3s ease-out reverse';
setTimeout(() => notification.remove(), 300);
}, 2000);
}
updateUI() {
const list = this.powerUpBar.querySelector('.power-up-list');
const now = Date.now();
if (this.activePowerUps.size === 0) {
this.powerUpBar.style.display = 'none';
return;
}
this.powerUpBar.style.display = 'block';
list.innerHTML = Array.from(this.activePowerUps.values())
.filter(pu => pu.expiresAt > now)
.map(pu => {
const remaining = pu.expiresAt - now;
const seconds = Math.ceil(remaining / 1000);
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
const progress = (remaining / pu.duration) * 100;
return `
<div class="power-up-item ${pu.legendary ? 'legendary' : ''}" style="border-color: ${pu.color}">
<div class="power-up-icon">${pu.icon}</div>
<div class="power-up-info">
<div class="power-up-name">${pu.name}</div>
<div class="power-up-timer">${minutes}:${secs.toString().padStart(2, '0')}</div>
</div>
<div class="power-up-progress" style="width: ${progress}%; background: ${pu.color}"></div>
</div>
`;
}).join('');
}
grantRandomPowerUp() {
const types = Object.keys(this.powerUpTypes).filter(t => t !== 'golden');
const randomType = types[Math.floor(Math.random() * types.length)];
return this.activatePowerUp(randomType);
}
getActivePowerUps() {
return Array.from(this.activePowerUps.values()).filter(pu => Date.now() < pu.expiresAt);
}
}

203
src/js/profileManager.js Normal file
View File

@@ -0,0 +1,203 @@
// Profile Customization for RoadWorld
// Allows players to customize their username and avatar
export class ProfileManager {
constructor(gameEngine, storageManager) {
this.gameEngine = gameEngine;
this.storageManager = storageManager;
this.panelElement = null;
this.isVisible = false;
// Avatar options
this.avatarIcons = ['🧑‍🚀', '🧙', '🦸', '🥷', '🧝', '🧛', '🤖', '👽', '🐱', '🐺', '🦊', '🐉'];
this.avatarColors = [
{ name: 'Cyan', value: '#00d4ff' },
{ name: 'Purple', value: '#7b2ff7' },
{ name: 'Orange', value: '#FF6B00' },
{ name: 'Pink', value: '#FF0066' },
{ name: 'Green', value: '#00FF88' },
{ name: 'Gold', value: '#FFD700' },
{ name: 'Red', value: '#FF4444' },
{ name: 'Blue', value: '#4444FF' }
];
}
init() {
this.createPanel();
console.log('👤 Profile Manager initialized');
}
createPanel() {
this.panelElement = document.createElement('div');
this.panelElement.className = 'profile-panel ui-overlay';
this.panelElement.id = 'profile-panel';
this.panelElement.style.display = 'none';
this.panelElement.innerHTML = `
<div class="panel-header">
<span>👤 Edit Profile</span>
<button class="panel-close" id="profile-close">✕</button>
</div>
<div class="panel-content profile-content">
<div class="profile-preview" id="profile-preview"></div>
<div class="profile-section">
<label class="profile-label">Username</label>
<input type="text" id="profile-username" class="profile-input" maxlength="20" placeholder="Enter username" />
</div>
<div class="profile-section">
<label class="profile-label">Avatar Icon</label>
<div class="avatar-grid" id="avatar-icons"></div>
</div>
<div class="profile-section">
<label class="profile-label">Avatar Color</label>
<div class="color-grid" id="avatar-colors"></div>
</div>
<button class="tool-btn primary" id="profile-save">Save Changes</button>
</div>
`;
document.body.appendChild(this.panelElement);
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('profile-close').addEventListener('click', () => {
this.hide();
});
document.getElementById('profile-save').addEventListener('click', () => {
this.saveProfile();
});
document.getElementById('profile-username').addEventListener('input', () => {
this.updatePreview();
});
}
renderOptions() {
const player = this.gameEngine.player;
// Render icon grid
const iconsEl = document.getElementById('avatar-icons');
iconsEl.innerHTML = this.avatarIcons.map(icon => `
<button class="avatar-option ${player.avatar.icon === icon ? 'selected' : ''}"
data-icon="${icon}">${icon}</button>
`).join('');
// Add click handlers
iconsEl.querySelectorAll('.avatar-option').forEach(btn => {
btn.addEventListener('click', () => {
iconsEl.querySelectorAll('.avatar-option').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
this.updatePreview();
});
});
// Render color grid
const colorsEl = document.getElementById('avatar-colors');
colorsEl.innerHTML = this.avatarColors.map(color => `
<button class="color-option ${player.avatar.color === color.value ? 'selected' : ''}"
data-color="${color.value}"
style="background: ${color.value}"
title="${color.name}"></button>
`).join('');
// Add click handlers
colorsEl.querySelectorAll('.color-option').forEach(btn => {
btn.addEventListener('click', () => {
colorsEl.querySelectorAll('.color-option').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
this.updatePreview();
});
});
// Set username
document.getElementById('profile-username').value = player.username;
this.updatePreview();
}
updatePreview() {
const player = this.gameEngine.player;
const previewEl = document.getElementById('profile-preview');
const selectedIcon = document.querySelector('#avatar-icons .selected')?.dataset.icon || player.avatar.icon || '🧑‍🚀';
const selectedColor = document.querySelector('#avatar-colors .selected')?.dataset.color || player.avatar.color;
const username = document.getElementById('profile-username').value || player.username;
previewEl.innerHTML = `
<div class="preview-avatar" style="background: ${selectedColor}">
<span class="preview-icon">${selectedIcon}</span>
</div>
<div class="preview-info">
<div class="preview-name">${username}</div>
<div class="preview-level">Level ${player.level}</div>
</div>
`;
}
saveProfile() {
const player = this.gameEngine.player;
const username = document.getElementById('profile-username').value.trim();
const selectedIcon = document.querySelector('#avatar-icons .selected')?.dataset.icon;
const selectedColor = document.querySelector('#avatar-colors .selected')?.dataset.color;
if (username) {
player.username = username;
}
if (selectedIcon) {
player.avatar.icon = selectedIcon;
}
if (selectedColor) {
player.avatar.color = selectedColor;
}
this.gameEngine.savePlayer();
// Trigger avatar update if visible
if (window.app && window.app.playerAvatar) {
window.app.playerAvatar.updateAppearance();
}
this.hide();
// Show confirmation
this.showNotification('Profile updated!');
}
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);
}
show() {
this.isVisible = true;
this.panelElement.style.display = 'block';
this.renderOptions();
}
hide() {
this.isVisible = false;
this.panelElement.style.display = 'none';
}
toggle() {
if (this.isVisible) {
this.hide();
} else {
this.show();
}
}
}

280
src/js/quickTravel.js Normal file
View File

@@ -0,0 +1,280 @@
// Quick Travel System for RoadWorld
// Allows instant teleportation to saved locations
export class QuickTravel {
constructor(mapManager, gameEngine, storageManager) {
this.mapManager = mapManager;
this.gameEngine = gameEngine;
this.storageManager = storageManager;
this.panelElement = null;
this.isVisible = false;
// Quick travel cooldown
this.lastTravelTime = 0;
this.cooldownDuration = 30000; // 30 seconds
}
init() {
this.createPanel();
console.log('🚀 Quick Travel initialized');
}
createPanel() {
this.panelElement = document.createElement('div');
this.panelElement.className = 'quick-travel-panel ui-overlay';
this.panelElement.id = 'quick-travel-panel';
this.panelElement.style.display = 'none';
this.panelElement.innerHTML = `
<div class="panel-header">
<span>🚀 Quick Travel</span>
<button class="panel-close" id="quick-travel-close">✕</button>
</div>
<div class="panel-content quick-travel-content">
<div class="quick-travel-cooldown" id="travel-cooldown" style="display: none;">
<div class="cooldown-text">Cooldown: <span id="cooldown-timer">0s</span></div>
<div class="cooldown-bar">
<div class="cooldown-fill" id="cooldown-fill"></div>
</div>
</div>
<div class="quick-travel-sections">
<div class="quick-travel-section">
<h4>📍 Saved Locations</h4>
<div class="travel-list" id="saved-travel-list"></div>
</div>
<div class="quick-travel-section">
<h4>🌍 Famous Landmarks</h4>
<div class="travel-list" id="landmarks-list"></div>
</div>
<div class="quick-travel-section">
<h4>🎲 Random Destination</h4>
<button class="travel-random-btn" id="random-travel">
🎲 Teleport Somewhere Random
</button>
</div>
</div>
</div>
`;
document.body.appendChild(this.panelElement);
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('quick-travel-close').addEventListener('click', () => {
this.hide();
});
document.getElementById('random-travel').addEventListener('click', () => {
this.travelToRandom();
});
}
renderLocations() {
// Render saved locations
const savedLocations = this.storageManager.getSavedLocations();
const savedList = document.getElementById('saved-travel-list');
if (savedLocations.length === 0) {
savedList.innerHTML = `
<div class="travel-empty">
No saved locations yet.<br>
Save locations using the 💾 button!
</div>
`;
} else {
savedList.innerHTML = savedLocations.map(loc => `
<button class="travel-btn" data-lat="${loc.lat}" data-lng="${loc.lng}" data-zoom="${loc.zoom}">
<span class="travel-icon">📍</span>
<span class="travel-name">${loc.name}</span>
<span class="travel-coords">${loc.lat.toFixed(2)}°, ${loc.lng.toFixed(2)}°</span>
</button>
`).join('');
}
// Render landmarks
const landmarks = [
{ name: 'Times Square, NYC', lat: 40.7580, lng: -73.9855, icon: '🗽' },
{ name: 'Eiffel Tower, Paris', lat: 48.8584, lng: 2.2945, icon: '🗼' },
{ name: 'Big Ben, London', lat: 51.5007, lng: -0.1246, icon: '🇬🇧' },
{ name: 'Shibuya, Tokyo', lat: 35.6595, lng: 139.7004, icon: '🗾' },
{ name: 'Colosseum, Rome', lat: 41.8902, lng: 12.4922, icon: '🏛️' },
{ name: 'Sydney Opera House', lat: -33.8568, lng: 151.2153, icon: '🎭' },
{ name: 'Machu Picchu, Peru', lat: -13.1631, lng: -72.5450, icon: '🏔️' },
{ name: 'Pyramids, Egypt', lat: 29.9792, lng: 31.1342, icon: '🏜️' },
{ name: 'Taj Mahal, India', lat: 27.1751, lng: 78.0421, icon: '🕌' },
{ name: 'Grand Canyon, USA', lat: 36.1069, lng: -112.1129, icon: '🏜️' }
];
const landmarksList = document.getElementById('landmarks-list');
landmarksList.innerHTML = landmarks.map(loc => `
<button class="travel-btn" data-lat="${loc.lat}" data-lng="${loc.lng}" data-zoom="17">
<span class="travel-icon">${loc.icon}</span>
<span class="travel-name">${loc.name}</span>
</button>
`).join('');
// Add click handlers
this.panelElement.querySelectorAll('.travel-btn').forEach(btn => {
btn.addEventListener('click', () => {
const lat = parseFloat(btn.dataset.lat);
const lng = parseFloat(btn.dataset.lng);
const zoom = parseFloat(btn.dataset.zoom) || 17;
this.travelTo([lng, lat], zoom);
});
});
// Update cooldown display
this.updateCooldown();
}
canTravel() {
const now = Date.now();
return now - this.lastTravelTime >= this.cooldownDuration;
}
getCooldownRemaining() {
const elapsed = Date.now() - this.lastTravelTime;
return Math.max(0, this.cooldownDuration - elapsed);
}
updateCooldown() {
const cooldownEl = document.getElementById('travel-cooldown');
const remaining = this.getCooldownRemaining();
if (remaining > 0) {
cooldownEl.style.display = 'block';
document.getElementById('cooldown-timer').textContent =
`${Math.ceil(remaining / 1000)}s`;
document.getElementById('cooldown-fill').style.width =
`${(remaining / this.cooldownDuration) * 100}%`;
// Disable buttons
this.panelElement.querySelectorAll('.travel-btn, .travel-random-btn').forEach(btn => {
btn.disabled = true;
btn.classList.add('disabled');
});
} else {
cooldownEl.style.display = 'none';
// Enable buttons
this.panelElement.querySelectorAll('.travel-btn, .travel-random-btn').forEach(btn => {
btn.disabled = false;
btn.classList.remove('disabled');
});
}
}
travelTo(lngLat, zoom = 17) {
if (!this.canTravel()) {
this.showNotification('Quick travel on cooldown!');
return false;
}
// Start travel animation
this.showTravelAnimation();
setTimeout(() => {
// Update map position
this.mapManager.flyTo({
center: lngLat,
zoom: zoom,
pitch: 60,
bearing: Math.random() * 60 - 30,
duration: 2000
});
// Update player position
if (this.gameEngine && this.gameEngine.player) {
this.gameEngine.player.position = lngLat;
this.gameEngine.savePlayer();
// Update avatar
if (window.app && window.app.playerAvatar) {
window.app.playerAvatar.updatePosition(lngLat);
}
}
// Start cooldown
this.lastTravelTime = Date.now();
// Close panel
this.hide();
this.showNotification('Teleported!');
}, 500);
return true;
}
travelToRandom() {
// Generate random location on land (approximation)
const landCoords = [
{ lat: [25, 50], lng: [-125, -70] }, // North America
{ lat: [35, 60], lng: [-10, 40] }, // Europe
{ lat: [20, 45], lng: [100, 145] }, // Asia
{ lat: [-35, -10], lng: [115, 155] }, // Australia
{ lat: [-55, 5], lng: [-80, -35] }, // South America
{ lat: [-35, 35], lng: [15, 50] } // Africa
];
const region = landCoords[Math.floor(Math.random() * landCoords.length)];
const lat = region.lat[0] + Math.random() * (region.lat[1] - region.lat[0]);
const lng = region.lng[0] + Math.random() * (region.lng[1] - region.lng[0]);
this.travelTo([lng, lat], 14);
}
showTravelAnimation() {
const overlay = document.createElement('div');
overlay.className = 'travel-animation';
overlay.innerHTML = `
<div class="travel-flash"></div>
<div class="travel-text">TELEPORTING...</div>
`;
document.body.appendChild(overlay);
setTimeout(() => {
overlay.classList.add('fade-out');
setTimeout(() => overlay.remove(), 500);
}, 1000);
}
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);
}
show() {
this.isVisible = true;
this.panelElement.style.display = 'block';
this.renderLocations();
// Start cooldown update loop
this.cooldownInterval = setInterval(() => this.updateCooldown(), 1000);
}
hide() {
this.isVisible = false;
this.panelElement.style.display = 'none';
if (this.cooldownInterval) {
clearInterval(this.cooldownInterval);
}
}
toggle() {
if (this.isVisible) {
this.hide();
} else {
this.show();
}
}
}

245
src/js/screenshotCapture.js Normal file
View File

@@ -0,0 +1,245 @@
// Screenshot Capture for RoadWorld
// Captures and exports the current map view
export class ScreenshotCapture {
constructor(mapManager) {
this.mapManager = mapManager;
this.isCapturing = false;
}
async capture(options = {}) {
if (this.isCapturing) return null;
this.isCapturing = true;
const {
includeUI = false,
format = 'png',
quality = 0.95,
watermark = true
} = options;
try {
// Get the map canvas
const mapCanvas = this.mapManager.map.getCanvas();
// Create a new canvas for the screenshot
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (includeUI) {
// Capture entire viewport including UI
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Use html2canvas if available, otherwise just the map
ctx.drawImage(mapCanvas, 0, 0, canvas.width, canvas.height);
} else {
// Just the map
canvas.width = mapCanvas.width;
canvas.height = mapCanvas.height;
ctx.drawImage(mapCanvas, 0, 0);
}
// Add watermark
if (watermark) {
this.addWatermark(ctx, canvas);
}
// Add location info
this.addLocationInfo(ctx, canvas);
// Convert to blob
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, `image/${format}`, quality);
});
this.isCapturing = false;
return {
blob,
canvas,
dataUrl: canvas.toDataURL(`image/${format}`, quality)
};
} catch (error) {
console.error('Screenshot capture failed:', error);
this.isCapturing = false;
return null;
}
}
addWatermark(ctx, canvas) {
const padding = 20;
// Gradient background for watermark
const gradient = ctx.createLinearGradient(padding, canvas.height - 60, padding + 200, canvas.height - 60);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0.8)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, canvas.height - 60, 300, 60);
// Logo text
ctx.font = 'bold 16px Orbitron, sans-serif';
ctx.fillStyle = '#00d4ff';
ctx.fillText('BLACKROAD EARTH', padding, canvas.height - 35);
ctx.font = '10px Orbitron, sans-serif';
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.fillText('ROADWORLD', padding, canvas.height - 20);
}
addLocationInfo(ctx, canvas) {
const center = this.mapManager.getCenter();
const zoom = this.mapManager.getZoom();
const padding = 20;
// Right side info
const infoX = canvas.width - padding;
const infoY = canvas.height - 20;
ctx.font = '12px monospace';
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.textAlign = 'right';
const coords = `${center.lat.toFixed(6)}°, ${center.lng.toFixed(6)}°`;
const zoomText = `Zoom: ${zoom.toFixed(1)}`;
const timestamp = new Date().toLocaleString();
ctx.fillText(coords, infoX, infoY);
ctx.fillText(zoomText, infoX, infoY - 15);
ctx.font = '10px monospace';
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillText(timestamp, infoX, infoY - 30);
ctx.textAlign = 'left';
}
async download(filename = null) {
const result = await this.capture();
if (!result) return false;
// Generate filename
const center = this.mapManager.getCenter();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const defaultName = `roadworld_${center.lat.toFixed(4)}_${center.lng.toFixed(4)}_${timestamp}.png`;
// Create download link
const link = document.createElement('a');
link.href = result.dataUrl;
link.download = filename || defaultName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
return true;
}
async copyToClipboard() {
const result = await this.capture();
if (!result) return false;
try {
await navigator.clipboard.write([
new ClipboardItem({
'image/png': result.blob
})
]);
return true;
} catch (error) {
console.error('Failed to copy to clipboard:', error);
return false;
}
}
async share() {
const result = await this.capture();
if (!result) return false;
// Check if Web Share API is available
if (!navigator.share) {
// Fallback to download
return this.download();
}
const center = this.mapManager.getCenter();
const file = new File([result.blob], 'roadworld_screenshot.png', { type: 'image/png' });
try {
await navigator.share({
title: 'RoadWorld Screenshot',
text: `Check out this location! ${center.lat.toFixed(4)}°, ${center.lng.toFixed(4)}°`,
files: [file]
});
return true;
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Share failed:', error);
}
return false;
}
}
// Create a preview of the screenshot
async preview() {
const result = await this.capture();
if (!result) return;
// Create preview overlay
const overlay = document.createElement('div');
overlay.className = 'screenshot-preview-overlay';
overlay.innerHTML = `
<div class="screenshot-preview-container">
<div class="screenshot-preview-header">
<span>Screenshot Preview</span>
<button class="screenshot-close">✕</button>
</div>
<div class="screenshot-preview-image">
<img src="${result.dataUrl}" alt="Screenshot" />
</div>
<div class="screenshot-preview-actions">
<button class="screenshot-action" data-action="download">
<span>💾</span> Download
</button>
<button class="screenshot-action" data-action="copy">
<span>📋</span> Copy
</button>
<button class="screenshot-action" data-action="share">
<span>🔗</span> Share
</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Event handlers
overlay.querySelector('.screenshot-close').onclick = () => overlay.remove();
overlay.onclick = (e) => {
if (e.target === overlay) overlay.remove();
};
overlay.querySelectorAll('.screenshot-action').forEach(btn => {
btn.onclick = async () => {
const action = btn.dataset.action;
let success = false;
switch (action) {
case 'download':
success = await this.download();
break;
case 'copy':
success = await this.copyToClipboard();
break;
case 'share':
success = await this.share();
break;
}
if (success) {
overlay.remove();
}
};
});
}
}

218
src/js/streakManager.js Normal file
View File

@@ -0,0 +1,218 @@
// Streak & Daily Rewards System for RoadWorld
// Tracks consecutive login days and awards bonuses
export class StreakManager {
constructor(gameEngine, storageManager) {
this.gameEngine = gameEngine;
this.storageManager = storageManager;
// Load streak data
this.streakData = this.loadStreakData();
// Define daily rewards
this.dailyRewards = this.defineDailyRewards();
}
defineDailyRewards() {
return [
{ day: 1, xp: 50, items: { stars: 5 }, bonus: null },
{ day: 2, xp: 75, items: { stars: 10 }, bonus: null },
{ day: 3, xp: 100, items: { stars: 10, gems: 2 }, bonus: '1.25x XP for 1 hour' },
{ day: 4, xp: 125, items: { stars: 15, gems: 3 }, bonus: null },
{ day: 5, xp: 150, items: { stars: 20, gems: 5 }, bonus: null },
{ day: 6, xp: 200, items: { stars: 25, gems: 5, trophies: 1 }, bonus: '1.5x XP for 1 hour' },
{ day: 7, xp: 500, items: { stars: 50, gems: 10, trophies: 2, keys: 1 }, bonus: '2x XP for 2 hours' }
];
}
loadStreakData() {
const data = this.storageManager.data.streak || {
currentStreak: 0,
longestStreak: 0,
lastLoginDate: null,
totalLogins: 0,
todayClaimed: false,
activeBonus: null
};
return data;
}
saveStreakData() {
this.storageManager.data.streak = this.streakData;
this.storageManager.save();
}
checkLogin() {
const today = new Date().toDateString();
const lastLogin = this.streakData.lastLoginDate;
if (lastLogin === today) {
// Already logged in today
return {
isNewDay: false,
streak: this.streakData.currentStreak,
reward: null
};
}
// Calculate days since last login
let streakContinues = false;
if (lastLogin) {
const lastDate = new Date(lastLogin);
const todayDate = new Date(today);
const diffTime = todayDate - lastDate;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 1) {
// Consecutive day
streakContinues = true;
this.streakData.currentStreak++;
} else if (diffDays > 1) {
// Streak broken
this.streakData.currentStreak = 1;
}
} else {
// First login
this.streakData.currentStreak = 1;
}
// Update stats
this.streakData.lastLoginDate = today;
this.streakData.totalLogins++;
this.streakData.todayClaimed = false;
// Update longest streak
if (this.streakData.currentStreak > this.streakData.longestStreak) {
this.streakData.longestStreak = this.streakData.currentStreak;
}
this.saveStreakData();
return {
isNewDay: true,
streak: this.streakData.currentStreak,
streakContinues,
reward: this.getTodayReward()
};
}
getTodayReward() {
const dayIndex = ((this.streakData.currentStreak - 1) % 7);
return this.dailyRewards[dayIndex];
}
claimDailyReward() {
if (this.streakData.todayClaimed) {
return { success: false, message: 'Already claimed today!' };
}
const reward = this.getTodayReward();
// Award XP
if (this.gameEngine && reward.xp) {
this.gameEngine.addXP(reward.xp, 'daily login reward');
}
// Award items
if (this.gameEngine && reward.items) {
const player = this.gameEngine.player;
for (const [item, count] of Object.entries(reward.items)) {
player.inventory[item] = (player.inventory[item] || 0) + count;
}
this.gameEngine.savePlayer();
}
// Apply bonus
if (reward.bonus) {
this.activateBonus(reward.bonus);
}
this.streakData.todayClaimed = true;
this.saveStreakData();
return {
success: true,
reward,
streak: this.streakData.currentStreak
};
}
activateBonus(bonusString) {
// Parse bonus string like "1.5x XP for 1 hour"
const match = bonusString.match(/([\d.]+)x XP for (\d+) hour/);
if (!match) return;
const multiplier = parseFloat(match[1]);
const hours = parseInt(match[2]);
const expiresAt = Date.now() + hours * 60 * 60 * 1000;
this.streakData.activeBonus = {
type: 'xp_multiplier',
multiplier,
expiresAt,
description: bonusString
};
this.saveStreakData();
console.log(`✨ Bonus activated: ${bonusString}`);
}
getActiveBonus() {
if (!this.streakData.activeBonus) return null;
if (Date.now() > this.streakData.activeBonus.expiresAt) {
// Bonus expired
this.streakData.activeBonus = null;
this.saveStreakData();
return null;
}
return this.streakData.activeBonus;
}
getXPMultiplier() {
const bonus = this.getActiveBonus();
return bonus ? bonus.multiplier : 1;
}
getStreakInfo() {
return {
currentStreak: this.streakData.currentStreak,
longestStreak: this.streakData.longestStreak,
totalLogins: this.streakData.totalLogins,
todayClaimed: this.streakData.todayClaimed,
todayReward: this.getTodayReward(),
activeBonus: this.getActiveBonus(),
weekProgress: this.getWeekProgress()
};
}
getWeekProgress() {
const progress = [];
const currentDay = ((this.streakData.currentStreak - 1) % 7) + 1;
for (let i = 0; i < 7; i++) {
const dayNum = i + 1;
progress.push({
day: dayNum,
reward: this.dailyRewards[i],
completed: this.streakData.todayClaimed ? dayNum <= currentDay : dayNum < currentDay,
current: dayNum === currentDay,
locked: dayNum > currentDay
});
}
return progress;
}
getBonusTimeRemaining() {
const bonus = this.getActiveBonus();
if (!bonus) return null;
const remaining = bonus.expiresAt - Date.now();
const hours = Math.floor(remaining / (1000 * 60 * 60));
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
return { hours, minutes, formatted: `${hours}h ${minutes}m` };
}
}