mirror of
https://github.com/blackboxprogramming/blackroad-roadworld.git
synced 2026-03-18 01:34:02 -05:00
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:
@@ -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 -->
|
||||||
|
|||||||
972
src/css/main.css
972
src/css/main.css
@@ -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
200
src/js/comboSystem.js
Normal 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
227
src/js/keyboardControls.js
Normal 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
256
src/js/leaderboardPanel.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/js/main.js
259
src/js/main.js
@@ -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
339
src/js/particleEffects.js
Normal 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
268
src/js/powerUpsManager.js
Normal 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
203
src/js/profileManager.js
Normal 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
280
src/js/quickTravel.js
Normal 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
245
src/js/screenshotCapture.js
Normal 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
218
src/js/streakManager.js
Normal 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` };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user