Complete deployment of unified Light Trinity system: 🔴 RedLight: Template & brand system (18 HTML templates) 💚 GreenLight: Project & collaboration (14 layers, 103 templates) 💛 YellowLight: Infrastructure & deployment 🌈 Trinity: Unified compliance & testing Includes: - 12 documentation files - 8 shell scripts - 18 HTML brand templates - Trinity compliance workflow Built by: Cece + Alexa Date: December 23, 2025 Source: blackroad-os/blackroad-os-infra 🌸✨
1788 lines
59 KiB
HTML
1788 lines
59 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BlackRoad World — The Game</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
overflow: hidden;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
|
|
background: #000;
|
|
}
|
|
#canvas-container { position: fixed; inset: 0; }
|
|
|
|
/* === TOP BAR === */
|
|
.top-bar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 60px;
|
|
background: linear-gradient(180deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 20px;
|
|
z-index: 100;
|
|
gap: 20px;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.logo-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
background: linear-gradient(135deg, #FF1D6C, #F5A623);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
}
|
|
.logo-text {
|
|
color: white;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* Resources */
|
|
.resources {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-left: auto;
|
|
}
|
|
.resource {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: rgba(255,255,255,0.1);
|
|
padding: 6px 12px;
|
|
border-radius: 20px;
|
|
color: white;
|
|
font-size: 14px;
|
|
}
|
|
.resource-icon { font-size: 16px; }
|
|
.resource-value { font-weight: 600; }
|
|
|
|
/* Level */
|
|
.level-display {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: white;
|
|
}
|
|
.level-badge {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, #FF1D6C, #F5A623);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 16px;
|
|
}
|
|
.xp-bar {
|
|
width: 100px;
|
|
height: 8px;
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.xp-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #FF1D6C, #F5A623);
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
/* === LEFT PANEL - QUESTS === */
|
|
.quest-panel {
|
|
position: fixed;
|
|
top: 80px;
|
|
left: 20px;
|
|
width: 280px;
|
|
background: rgba(0,0,0,0.85);
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.panel-title {
|
|
color: white;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.quest-item {
|
|
background: rgba(255,255,255,0.05);
|
|
border-radius: 10px;
|
|
padding: 12px;
|
|
margin-bottom: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.quest-item:hover { background: rgba(255,255,255,0.1); }
|
|
.quest-item.completed { opacity: 0.5; }
|
|
.quest-name {
|
|
color: white;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
margin-bottom: 4px;
|
|
}
|
|
.quest-desc {
|
|
color: rgba(255,255,255,0.6);
|
|
font-size: 11px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.quest-progress {
|
|
height: 4px;
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
.quest-progress-fill {
|
|
height: 100%;
|
|
background: #4CAF50;
|
|
transition: width 0.3s;
|
|
}
|
|
.quest-reward {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 6px;
|
|
font-size: 11px;
|
|
color: #F5A623;
|
|
}
|
|
|
|
/* === RIGHT PANEL - INVENTORY === */
|
|
.inventory-panel {
|
|
position: fixed;
|
|
top: 80px;
|
|
right: 20px;
|
|
width: 280px;
|
|
background: rgba(0,0,0,0.85);
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.inventory-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 8px;
|
|
}
|
|
.inv-slot {
|
|
aspect-ratio: 1;
|
|
background: rgba(255,255,255,0.05);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
border: 2px solid transparent;
|
|
}
|
|
.inv-slot:hover { background: rgba(255,255,255,0.1); }
|
|
.inv-slot.selected { border-color: #FF1D6C; }
|
|
.inv-slot-icon { font-size: 24px; }
|
|
.inv-slot-count {
|
|
position: absolute;
|
|
bottom: 2px;
|
|
right: 4px;
|
|
font-size: 10px;
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* === BOTTOM BAR - BUILD MENU === */
|
|
.build-bar {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 8px;
|
|
background: rgba(0,0,0,0.85);
|
|
padding: 12px 20px;
|
|
border-radius: 50px;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.build-btn {
|
|
width: 56px;
|
|
height: 56px;
|
|
background: rgba(255,255,255,0.1);
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
position: relative;
|
|
}
|
|
.build-btn:hover { background: rgba(255,255,255,0.2); transform: translateY(-4px); }
|
|
.build-btn.active { background: #FF1D6C; }
|
|
.build-btn.locked { opacity: 0.4; cursor: not-allowed; }
|
|
.build-btn-cost {
|
|
font-size: 9px;
|
|
color: #F5A623;
|
|
position: absolute;
|
|
bottom: 4px;
|
|
}
|
|
.build-btn-key {
|
|
position: absolute;
|
|
top: 2px;
|
|
right: 4px;
|
|
font-size: 9px;
|
|
color: rgba(255,255,255,0.5);
|
|
}
|
|
|
|
/* === STATS PANEL === */
|
|
.stats-panel {
|
|
position: fixed;
|
|
bottom: 100px;
|
|
left: 20px;
|
|
background: rgba(0,0,0,0.85);
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.stat-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.stat-icon { font-size: 18px; }
|
|
.stat-bar {
|
|
width: 100px;
|
|
height: 8px;
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.stat-fill { height: 100%; transition: width 0.3s; }
|
|
.stat-fill.happiness { background: #FF69B4; }
|
|
.stat-fill.nature { background: #4CAF50; }
|
|
.stat-fill.energy { background: #F5A623; }
|
|
.stat-fill.magic { background: #9C27B0; }
|
|
.stat-value {
|
|
color: white;
|
|
font-size: 12px;
|
|
width: 35px;
|
|
}
|
|
|
|
/* === CONTROLS === */
|
|
.controls {
|
|
position: fixed;
|
|
bottom: 100px;
|
|
right: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
z-index: 100;
|
|
}
|
|
.ctrl-btn {
|
|
width: 48px;
|
|
height: 48px;
|
|
background: rgba(0,0,0,0.85);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
border-radius: 12px;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.ctrl-btn:hover { background: rgba(255,255,255,0.2); }
|
|
.ctrl-btn.active { background: #FF1D6C; border-color: #FF1D6C; }
|
|
|
|
/* === TIME & WEATHER === */
|
|
.time-display {
|
|
position: fixed;
|
|
top: 80px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0,0,0,0.85);
|
|
padding: 10px 24px;
|
|
border-radius: 50px;
|
|
z-index: 100;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.time-text {
|
|
color: white;
|
|
font-size: 20px;
|
|
font-weight: 300;
|
|
}
|
|
.day-text {
|
|
color: rgba(255,255,255,0.6);
|
|
font-size: 12px;
|
|
}
|
|
.weather-icon { font-size: 24px; }
|
|
|
|
/* === NOTIFICATIONS === */
|
|
.notifications {
|
|
position: fixed;
|
|
top: 140px;
|
|
right: 20px;
|
|
width: 280px;
|
|
z-index: 200;
|
|
pointer-events: none;
|
|
}
|
|
.notification {
|
|
background: rgba(0,0,0,0.9);
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
margin-bottom: 8px;
|
|
color: white;
|
|
font-size: 13px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.7s;
|
|
border-left: 3px solid #4CAF50;
|
|
}
|
|
.notification.warning { border-left-color: #F5A623; }
|
|
.notification.error { border-left-color: #f44336; }
|
|
@keyframes slideIn { from { transform: translateX(100px); opacity: 0; } }
|
|
@keyframes fadeOut { to { opacity: 0; } }
|
|
|
|
/* === TOOLTIP === */
|
|
.tooltip {
|
|
position: fixed;
|
|
background: rgba(0,0,0,0.95);
|
|
border-radius: 8px;
|
|
padding: 10px 14px;
|
|
color: white;
|
|
font-size: 12px;
|
|
z-index: 300;
|
|
pointer-events: none;
|
|
max-width: 200px;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
display: none;
|
|
}
|
|
.tooltip-title { font-weight: 600; margin-bottom: 4px; }
|
|
.tooltip-desc { color: rgba(255,255,255,0.7); }
|
|
|
|
/* === LOADING === */
|
|
.loading {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
transition: opacity 0.5s;
|
|
}
|
|
.loading.hidden { opacity: 0; pointer-events: none; }
|
|
.loading-title {
|
|
font-size: 32px;
|
|
color: white;
|
|
margin-bottom: 8px;
|
|
font-weight: 300;
|
|
}
|
|
.loading-sub { color: rgba(255,255,255,0.6); margin-bottom: 30px; }
|
|
.loading-spinner {
|
|
width: 60px;
|
|
height: 60px;
|
|
border: 3px solid rgba(255,255,255,0.1);
|
|
border-top-color: #FF1D6C;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.loading-tip {
|
|
margin-top: 30px;
|
|
color: rgba(255,255,255,0.5);
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* === LEVEL UP MODAL === */
|
|
.modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 500;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.modal.visible { opacity: 1; pointer-events: auto; }
|
|
.modal-content {
|
|
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
|
border-radius: 20px;
|
|
padding: 40px;
|
|
text-align: center;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
transform: scale(0.9);
|
|
transition: transform 0.3s;
|
|
}
|
|
.modal.visible .modal-content { transform: scale(1); }
|
|
.modal-icon { font-size: 60px; margin-bottom: 16px; }
|
|
.modal-title { color: white; font-size: 28px; margin-bottom: 8px; }
|
|
.modal-desc { color: rgba(255,255,255,0.7); margin-bottom: 24px; }
|
|
.modal-rewards {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.modal-reward {
|
|
background: rgba(255,255,255,0.1);
|
|
padding: 12px 20px;
|
|
border-radius: 10px;
|
|
}
|
|
.modal-btn {
|
|
background: linear-gradient(135deg, #FF1D6C, #F5A623);
|
|
border: none;
|
|
padding: 12px 32px;
|
|
border-radius: 25px;
|
|
color: white;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
.modal-btn:hover { transform: scale(1.05); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Loading -->
|
|
<div class="loading" id="loading">
|
|
<div class="loading-title">BlackRoad World</div>
|
|
<div class="loading-sub">Building your world...</div>
|
|
<div class="loading-spinner"></div>
|
|
<div class="loading-tip">💡 Tip: Collect resources to build and expand your world!</div>
|
|
</div>
|
|
|
|
<div id="canvas-container"></div>
|
|
|
|
<!-- Top Bar -->
|
|
<div class="top-bar">
|
|
<div class="logo">
|
|
<div class="logo-icon">🌍</div>
|
|
<span class="logo-text">BlackRoad World</span>
|
|
</div>
|
|
|
|
<div class="level-display">
|
|
<div class="level-badge" id="levelBadge">1</div>
|
|
<div>
|
|
<div style="color: white; font-size: 12px;">Level <span id="levelNum">1</span></div>
|
|
<div class="xp-bar">
|
|
<div class="xp-fill" id="xpFill" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="resources">
|
|
<div class="resource">
|
|
<span class="resource-icon">🪵</span>
|
|
<span class="resource-value" id="resWood">50</span>
|
|
</div>
|
|
<div class="resource">
|
|
<span class="resource-icon">🪨</span>
|
|
<span class="resource-value" id="resStone">30</span>
|
|
</div>
|
|
<div class="resource">
|
|
<span class="resource-icon">🌾</span>
|
|
<span class="resource-value" id="resFood">100</span>
|
|
</div>
|
|
<div class="resource">
|
|
<span class="resource-icon">💎</span>
|
|
<span class="resource-value" id="resGems">10</span>
|
|
</div>
|
|
<div class="resource">
|
|
<span class="resource-icon">⚡</span>
|
|
<span class="resource-value" id="resEnergy">100</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Time Display -->
|
|
<div class="time-display">
|
|
<span class="weather-icon" id="weatherIcon">☀️</span>
|
|
<div>
|
|
<div class="time-text" id="timeText">12:00</div>
|
|
<div class="day-text">Day <span id="dayNum">1</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quest Panel -->
|
|
<div class="quest-panel">
|
|
<div class="panel-title">📜 Active Quests</div>
|
|
<div id="questList"></div>
|
|
</div>
|
|
|
|
<!-- Inventory Panel -->
|
|
<div class="inventory-panel">
|
|
<div class="panel-title">🎒 Inventory</div>
|
|
<div class="inventory-grid" id="inventoryGrid"></div>
|
|
</div>
|
|
|
|
<!-- Stats Panel -->
|
|
<div class="stats-panel">
|
|
<div class="stat-row">
|
|
<span class="stat-icon">💚</span>
|
|
<div class="stat-bar"><div class="stat-fill happiness" id="statHappiness" style="width:80%"></div></div>
|
|
<span class="stat-value" id="valHappiness">80%</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-icon">🌱</span>
|
|
<div class="stat-bar"><div class="stat-fill nature" id="statNature" style="width:70%"></div></div>
|
|
<span class="stat-value" id="valNature">70%</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-icon">⚡</span>
|
|
<div class="stat-bar"><div class="stat-fill energy" id="statEnergy" style="width:90%"></div></div>
|
|
<span class="stat-value" id="valEnergy">90%</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-icon">✨</span>
|
|
<div class="stat-bar"><div class="stat-fill magic" id="statMagic" style="width:50%"></div></div>
|
|
<span class="stat-value" id="valMagic">50%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Build Bar -->
|
|
<div class="build-bar">
|
|
<button class="build-btn" data-build="tree" title="Plant Tree">
|
|
🌳
|
|
<span class="build-btn-cost">5🪵</span>
|
|
<span class="build-btn-key">1</span>
|
|
</button>
|
|
<button class="build-btn" data-build="flower" title="Plant Flower">
|
|
🌸
|
|
<span class="build-btn-cost">2🌾</span>
|
|
<span class="build-btn-key">2</span>
|
|
</button>
|
|
<button class="build-btn" data-build="house" title="Build House">
|
|
🏠
|
|
<span class="build-btn-cost">20🪵 10🪨</span>
|
|
<span class="build-btn-key">3</span>
|
|
</button>
|
|
<button class="build-btn" data-build="farm" title="Build Farm">
|
|
🌾
|
|
<span class="build-btn-cost">15🪵 5🪨</span>
|
|
<span class="build-btn-key">4</span>
|
|
</button>
|
|
<button class="build-btn" data-build="windmill" title="Build Windmill">
|
|
🏭
|
|
<span class="build-btn-cost">30🪵 20🪨</span>
|
|
<span class="build-btn-key">5</span>
|
|
</button>
|
|
<button class="build-btn" data-build="agent" title="Spawn Agent">
|
|
🤖
|
|
<span class="build-btn-cost">50💎</span>
|
|
<span class="build-btn-key">6</span>
|
|
</button>
|
|
<button class="build-btn locked" data-build="portal" title="Build Portal (Lvl 5)">
|
|
🌀
|
|
<span class="build-btn-cost">100💎</span>
|
|
<span class="build-btn-key">7</span>
|
|
</button>
|
|
<button class="build-btn locked" data-build="castle" title="Build Castle (Lvl 10)">
|
|
🏰
|
|
<span class="build-btn-cost">200🪵 150🪨</span>
|
|
<span class="build-btn-key">8</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="controls">
|
|
<button class="ctrl-btn active" id="btnRotate" title="Auto Rotate">🔄</button>
|
|
<button class="ctrl-btn" id="btnDay" title="Day">☀️</button>
|
|
<button class="ctrl-btn" id="btnNight" title="Night">🌙</button>
|
|
<button class="ctrl-btn" id="btnRain" title="Rain">🌧️</button>
|
|
<button class="ctrl-btn" id="btnSpeed" title="Speed x2">⏩</button>
|
|
</div>
|
|
|
|
<!-- Notifications -->
|
|
<div class="notifications" id="notifications"></div>
|
|
|
|
<!-- Tooltip -->
|
|
<div class="tooltip" id="tooltip">
|
|
<div class="tooltip-title"></div>
|
|
<div class="tooltip-desc"></div>
|
|
</div>
|
|
|
|
<!-- Level Up Modal -->
|
|
<div class="modal" id="levelModal">
|
|
<div class="modal-content">
|
|
<div class="modal-icon">🎉</div>
|
|
<div class="modal-title">Level Up!</div>
|
|
<div class="modal-desc">You reached level <span id="modalLevel">2</span>!</div>
|
|
<div class="modal-rewards">
|
|
<div class="modal-reward">+50 🪵</div>
|
|
<div class="modal-reward">+30 🪨</div>
|
|
<div class="modal-reward">+10 💎</div>
|
|
</div>
|
|
<button class="modal-btn" onclick="closeModal()">Continue</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
// ==================== GAME STATE ====================
|
|
const GAME = {
|
|
level: 1,
|
|
xp: 0,
|
|
xpNeeded: 100,
|
|
day: 1,
|
|
hour: 12,
|
|
minute: 0,
|
|
speed: 1,
|
|
|
|
resources: {
|
|
wood: 50,
|
|
stone: 30,
|
|
food: 100,
|
|
gems: 10,
|
|
energy: 100
|
|
},
|
|
|
|
stats: {
|
|
happiness: 80,
|
|
nature: 70,
|
|
energy: 90,
|
|
magic: 50
|
|
},
|
|
|
|
inventory: [
|
|
{ id: 'axe', name: 'Axe', icon: '🪓', count: 1, desc: 'Chop trees faster' },
|
|
{ id: 'pickaxe', name: 'Pickaxe', icon: '⛏️', count: 1, desc: 'Mine stone' },
|
|
{ id: 'seeds', name: 'Seeds', icon: '🌱', count: 10, desc: 'Plant crops' },
|
|
{ id: 'water', name: 'Water Bucket', icon: '🪣', count: 5, desc: 'Water plants' },
|
|
{ id: 'magic_wand', name: 'Magic Wand', icon: '🪄', count: 1, desc: 'Cast spells' },
|
|
{ id: 'compass', name: 'Compass', icon: '🧭', count: 1, desc: 'Navigate the world' },
|
|
{ id: 'map', name: 'World Map', icon: '🗺️', count: 1, desc: 'View your territory' },
|
|
{ id: 'lantern', name: 'Lantern', icon: '🏮', count: 3, desc: 'Light up the night' },
|
|
],
|
|
|
|
quests: [
|
|
{ id: 'plant_trees', name: 'Forest Guardian', desc: 'Plant 10 trees', icon: '🌳', progress: 0, goal: 10, reward: { xp: 50, wood: 20 }, completed: false },
|
|
{ id: 'build_houses', name: 'Village Builder', desc: 'Build 5 houses', icon: '🏠', progress: 0, goal: 5, reward: { xp: 100, gems: 5 }, completed: false },
|
|
{ id: 'spawn_agents', name: 'AI Revolution', desc: 'Create 3 agents', icon: '🤖', progress: 0, goal: 3, reward: { xp: 150, magic: 20 }, completed: false },
|
|
{ id: 'flowers', name: 'Garden Master', desc: 'Plant 20 flowers', icon: '🌸', progress: 0, goal: 20, reward: { xp: 30, food: 50 }, completed: false },
|
|
{ id: 'survive_night', name: 'Night Owl', desc: 'Survive 3 nights', icon: '🌙', progress: 0, goal: 3, reward: { xp: 75, gems: 10 }, completed: false },
|
|
],
|
|
|
|
unlocks: {
|
|
5: ['portal'],
|
|
10: ['castle']
|
|
},
|
|
|
|
counts: {
|
|
trees: 0,
|
|
flowers: 0,
|
|
houses: 0,
|
|
agents: 0,
|
|
animals: 0
|
|
}
|
|
};
|
|
|
|
// ==================== THREE.JS SETUP ====================
|
|
let scene, camera, renderer;
|
|
let world, water;
|
|
let trees = [], flowers = [], houses = [], animals = [], agents = [];
|
|
let clouds = [], birds = [], rain = null;
|
|
let time = 0;
|
|
let autoRotate = true;
|
|
let isNight = false;
|
|
let isRaining = false;
|
|
let buildMode = null;
|
|
let selectedSlot = null;
|
|
|
|
// Camera
|
|
let isDragging = false;
|
|
let prevMouse = { x: 0, y: 0 };
|
|
let camAngle = 0, camHeight = 50, camDist = 100;
|
|
|
|
const COLORS = {
|
|
sky: 0x87CEEB,
|
|
skyNight: 0x1a1a2e,
|
|
grass: 0x7CBA3D,
|
|
water: 0x4FA4E8,
|
|
sand: 0xF4D03F
|
|
};
|
|
|
|
function init() {
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(COLORS.sky);
|
|
scene.fog = new THREE.Fog(COLORS.sky, 80, 300);
|
|
|
|
// Camera
|
|
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
updateCamera();
|
|
|
|
// Renderer
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.shadowMap.enabled = true;
|
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
|
|
|
// Build world
|
|
createLights();
|
|
createTerrain();
|
|
createWater();
|
|
createInitialWorld();
|
|
createClouds();
|
|
createRain();
|
|
|
|
// Events
|
|
setupEvents();
|
|
|
|
// UI
|
|
updateUI();
|
|
renderQuests();
|
|
renderInventory();
|
|
|
|
// Start
|
|
setTimeout(() => {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
notify('🌍 Welcome to BlackRoad World!', 'success');
|
|
animate();
|
|
}, 1500);
|
|
}
|
|
|
|
function createLights() {
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
|
|
|
window.sunLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
window.sunLight.position.set(80, 100, 50);
|
|
window.sunLight.castShadow = true;
|
|
window.sunLight.shadow.mapSize.set(2048, 2048);
|
|
window.sunLight.shadow.camera.near = 0.5;
|
|
window.sunLight.shadow.camera.far = 300;
|
|
window.sunLight.shadow.camera.left = -100;
|
|
window.sunLight.shadow.camera.right = 100;
|
|
window.sunLight.shadow.camera.top = 100;
|
|
window.sunLight.shadow.camera.bottom = -100;
|
|
scene.add(window.sunLight);
|
|
|
|
window.hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x7CBA3D, 0.3);
|
|
scene.add(window.hemiLight);
|
|
}
|
|
|
|
function createTerrain() {
|
|
const size = 150;
|
|
const geo = new THREE.PlaneGeometry(size, size, 80, 80);
|
|
const pos = geo.attributes.position.array;
|
|
|
|
for (let i = 0; i < pos.length; i += 3) {
|
|
const x = pos[i], y = pos[i + 1];
|
|
const dist = Math.sqrt(x * x + y * y);
|
|
let h = 0;
|
|
|
|
if (dist < size * 0.4) {
|
|
h = Math.sin(x * 0.06) * Math.cos(y * 0.06) * 6;
|
|
h += Math.sin(x * 0.12) * Math.cos(y * 0.12) * 3;
|
|
const edge = 1 - Math.pow(dist / (size * 0.4), 2);
|
|
h *= Math.max(0, edge);
|
|
h = Math.max(0, h);
|
|
} else {
|
|
h = -5;
|
|
}
|
|
pos[i + 2] = h;
|
|
}
|
|
geo.computeVertexNormals();
|
|
|
|
const mat = new THREE.MeshStandardMaterial({ color: COLORS.grass, roughness: 0.8 });
|
|
world = new THREE.Mesh(geo, mat);
|
|
world.rotation.x = -Math.PI / 2;
|
|
world.receiveShadow = true;
|
|
scene.add(world);
|
|
|
|
// Beach
|
|
const beach = new THREE.Mesh(
|
|
new THREE.RingGeometry(size * 0.38, size * 0.42, 64),
|
|
new THREE.MeshStandardMaterial({ color: COLORS.sand })
|
|
);
|
|
beach.rotation.x = -Math.PI / 2;
|
|
beach.position.y = 0.1;
|
|
scene.add(beach);
|
|
}
|
|
|
|
function createWater() {
|
|
water = new THREE.Mesh(
|
|
new THREE.CircleGeometry(200, 64),
|
|
new THREE.MeshStandardMaterial({
|
|
color: COLORS.water,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
roughness: 0.1
|
|
})
|
|
);
|
|
water.rotation.x = -Math.PI / 2;
|
|
water.position.y = -2;
|
|
scene.add(water);
|
|
}
|
|
|
|
function createInitialWorld() {
|
|
// Initial trees
|
|
for (let i = 0; i < 60; i++) spawnTree(null, true);
|
|
|
|
// Initial flowers
|
|
for (let i = 0; i < 100; i++) spawnFlower(null, true);
|
|
|
|
// Initial houses
|
|
for (let i = 0; i < 8; i++) spawnHouse(null, true);
|
|
|
|
// Initial animals
|
|
for (let i = 0; i < 20; i++) spawnAnimal();
|
|
|
|
// Initial agents
|
|
for (let i = 0; i < 5; i++) spawnAgent(null, true);
|
|
|
|
// Birds
|
|
for (let i = 0; i < 15; i++) spawnBird();
|
|
|
|
updateCounts();
|
|
}
|
|
|
|
function createClouds() {
|
|
for (let i = 0; i < 12; i++) {
|
|
const cloud = createCloud();
|
|
cloud.position.set(
|
|
(Math.random() - 0.5) * 150,
|
|
30 + Math.random() * 20,
|
|
(Math.random() - 0.5) * 150
|
|
);
|
|
clouds.push(cloud);
|
|
scene.add(cloud);
|
|
}
|
|
}
|
|
|
|
function createCloud() {
|
|
const group = new THREE.Group();
|
|
const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, transparent: true, opacity: 0.9 });
|
|
|
|
[[0,0,0,2.5], [-1.5,0,0,2], [1.5,0,0,2], [0,0.8,0,1.5], [-1,0.5,0,1.5], [1,0.5,0,1.5]].forEach(([x,y,z,r]) => {
|
|
const puff = new THREE.Mesh(new THREE.SphereGeometry(r, 8, 8), mat);
|
|
puff.position.set(x, y, z);
|
|
group.add(puff);
|
|
});
|
|
|
|
group.userData = { speed: 0.01 + Math.random() * 0.02 };
|
|
return group;
|
|
}
|
|
|
|
function createRain() {
|
|
const count = 5000;
|
|
const geo = new THREE.BufferGeometry();
|
|
const pos = new Float32Array(count * 3);
|
|
const vel = new Float32Array(count);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
pos[i * 3] = (Math.random() - 0.5) * 150;
|
|
pos[i * 3 + 1] = Math.random() * 80;
|
|
pos[i * 3 + 2] = (Math.random() - 0.5) * 150;
|
|
vel[i] = 0.5 + Math.random() * 0.5;
|
|
}
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
rain = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x8888ff, size: 0.15, transparent: true, opacity: 0.6 }));
|
|
rain.userData = { velocities: vel };
|
|
rain.visible = false;
|
|
scene.add(rain);
|
|
}
|
|
|
|
// ==================== SPAWNERS ====================
|
|
function getRandomPosition() {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 10 + Math.random() * 45;
|
|
return { x: Math.cos(angle) * dist, z: Math.sin(angle) * dist };
|
|
}
|
|
|
|
function spawnTree(pos, initial = false) {
|
|
const group = new THREE.Group();
|
|
const types = ['pine', 'oak', 'cherry'];
|
|
const type = types[Math.floor(Math.random() * types.length)];
|
|
|
|
// Trunk
|
|
const trunk = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(0.2, 0.35, 2, 6),
|
|
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
|
|
);
|
|
trunk.position.y = 1;
|
|
trunk.castShadow = true;
|
|
group.add(trunk);
|
|
|
|
// Leaves
|
|
if (type === 'pine') {
|
|
for (let i = 0; i < 3; i++) {
|
|
const cone = new THREE.Mesh(
|
|
new THREE.ConeGeometry(1.8 - i * 0.4, 2, 6),
|
|
new THREE.MeshStandardMaterial({ color: 0x228B22 })
|
|
);
|
|
cone.position.y = 2.5 + i * 1.2;
|
|
cone.castShadow = true;
|
|
group.add(cone);
|
|
}
|
|
} else if (type === 'cherry') {
|
|
const leaves = new THREE.Mesh(
|
|
new THREE.SphereGeometry(2, 8, 8),
|
|
new THREE.MeshStandardMaterial({ color: 0xFFB7C5 })
|
|
);
|
|
leaves.position.y = 3.5;
|
|
leaves.castShadow = true;
|
|
group.add(leaves);
|
|
} else {
|
|
const leaves = new THREE.Mesh(
|
|
new THREE.SphereGeometry(2, 8, 8),
|
|
new THREE.MeshStandardMaterial({ color: 0x32CD32 })
|
|
);
|
|
leaves.position.y = 3.5;
|
|
leaves.castShadow = true;
|
|
group.add(leaves);
|
|
}
|
|
|
|
const p = pos || getRandomPosition();
|
|
group.position.set(p.x, 0, p.z);
|
|
group.scale.setScalar(0.6 + Math.random() * 0.4);
|
|
group.userData = { type: 'tree', sway: Math.random() * Math.PI * 2 };
|
|
|
|
trees.push(group);
|
|
scene.add(group);
|
|
|
|
if (!initial) {
|
|
GAME.counts.trees++;
|
|
updateQuest('plant_trees', 1);
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
function spawnFlower(pos, initial = false) {
|
|
const group = new THREE.Group();
|
|
const colors = [0xFF69B4, 0xFFD700, 0xFF6347, 0x9370DB, 0x00CED1, 0xFFFFFF];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
const stem = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(0.03, 0.03, 0.6, 4),
|
|
new THREE.MeshStandardMaterial({ color: 0x228B22 })
|
|
);
|
|
stem.position.y = 0.3;
|
|
group.add(stem);
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const petal = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.15, 6, 6),
|
|
new THREE.MeshStandardMaterial({ color })
|
|
);
|
|
petal.scale.set(1, 0.3, 0.5);
|
|
petal.position.y = 0.6;
|
|
petal.position.x = Math.cos((i / 5) * Math.PI * 2) * 0.15;
|
|
petal.position.z = Math.sin((i / 5) * Math.PI * 2) * 0.15;
|
|
group.add(petal);
|
|
}
|
|
|
|
const center = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.08, 6, 6),
|
|
new THREE.MeshStandardMaterial({ color: 0xFFD700 })
|
|
);
|
|
center.position.y = 0.6;
|
|
group.add(center);
|
|
|
|
const p = pos || getRandomPosition();
|
|
group.position.set(p.x, 0, p.z);
|
|
group.scale.setScalar(0.5 + Math.random() * 0.5);
|
|
group.userData = { type: 'flower', sway: Math.random() * Math.PI * 2 };
|
|
|
|
flowers.push(group);
|
|
scene.add(group);
|
|
|
|
if (!initial) {
|
|
GAME.counts.flowers++;
|
|
updateQuest('flowers', 1);
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
function spawnHouse(pos, initial = false) {
|
|
const group = new THREE.Group();
|
|
const roofColors = [0xE74C3C, 0x3498DB, 0xFF1D6C, 0xF5A623, 0x9C27B0];
|
|
|
|
// Walls
|
|
const walls = new THREE.Mesh(
|
|
new THREE.BoxGeometry(3, 2.5, 3),
|
|
new THREE.MeshStandardMaterial({ color: 0xFAF0E6 })
|
|
);
|
|
walls.position.y = 1.25;
|
|
walls.castShadow = true;
|
|
walls.receiveShadow = true;
|
|
group.add(walls);
|
|
|
|
// Roof
|
|
const roof = new THREE.Mesh(
|
|
new THREE.ConeGeometry(2.8, 1.5, 4),
|
|
new THREE.MeshStandardMaterial({ color: roofColors[Math.floor(Math.random() * roofColors.length)] })
|
|
);
|
|
roof.position.y = 3.25;
|
|
roof.rotation.y = Math.PI / 4;
|
|
roof.castShadow = true;
|
|
group.add(roof);
|
|
|
|
// Door
|
|
const door = new THREE.Mesh(
|
|
new THREE.BoxGeometry(0.6, 1.2, 0.1),
|
|
new THREE.MeshStandardMaterial({ color: 0x8B4513 })
|
|
);
|
|
door.position.set(0, 0.6, 1.55);
|
|
group.add(door);
|
|
|
|
// Windows
|
|
const windowMat = new THREE.MeshStandardMaterial({ color: 0x87CEEB, emissive: 0x000000 });
|
|
[[-0.8, 1.5, 1.55], [0.8, 1.5, 1.55]].forEach(([x, y, z]) => {
|
|
const win = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.1), windowMat);
|
|
win.position.set(x, y, z);
|
|
group.add(win);
|
|
});
|
|
|
|
const p = pos || getRandomPosition();
|
|
group.position.set(p.x, 0, p.z);
|
|
group.rotation.y = Math.random() * Math.PI * 2;
|
|
group.scale.setScalar(0.7 + Math.random() * 0.3);
|
|
group.userData = { type: 'house', windows: windowMat };
|
|
|
|
houses.push(group);
|
|
scene.add(group);
|
|
|
|
if (!initial) {
|
|
GAME.counts.houses++;
|
|
updateQuest('build_houses', 1);
|
|
GAME.stats.happiness = Math.min(100, GAME.stats.happiness + 5);
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
function spawnAnimal() {
|
|
const group = new THREE.Group();
|
|
const colors = [0xFFFFFF, 0x8B4513, 0xFF6600, 0xD2B48C, 0x808080];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
const mat = new THREE.MeshStandardMaterial({ color });
|
|
|
|
const body = new THREE.Mesh(new THREE.SphereGeometry(0.4, 8, 8), mat);
|
|
body.scale.set(1, 0.7, 1.2);
|
|
group.add(body);
|
|
|
|
const head = new THREE.Mesh(new THREE.SphereGeometry(0.25, 8, 8), mat);
|
|
head.position.set(0, 0.2, 0.4);
|
|
group.add(head);
|
|
|
|
// Eyes
|
|
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
[[-0.08, 0.3, 0.55], [0.08, 0.3, 0.55]].forEach(([x, y, z]) => {
|
|
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.05, 6, 6), eyeMat);
|
|
eye.position.set(x, y, z);
|
|
group.add(eye);
|
|
});
|
|
|
|
// Ears
|
|
[[-0.1, 0.4, 0.3], [0.1, 0.4, 0.3]].forEach(([x, y, z]) => {
|
|
const ear = new THREE.Mesh(new THREE.ConeGeometry(0.08, 0.2, 6), mat);
|
|
ear.position.set(x, y, z);
|
|
group.add(ear);
|
|
});
|
|
|
|
const p = getRandomPosition();
|
|
group.position.set(p.x, 0.4, p.z);
|
|
group.scale.setScalar(0.5 + Math.random() * 0.3);
|
|
group.userData = {
|
|
type: 'animal',
|
|
targetX: p.x,
|
|
targetZ: p.z,
|
|
speed: 0.02 + Math.random() * 0.02,
|
|
hop: Math.random() * Math.PI * 2,
|
|
idle: 0
|
|
};
|
|
|
|
animals.push(group);
|
|
scene.add(group);
|
|
GAME.counts.animals++;
|
|
return group;
|
|
}
|
|
|
|
function spawnAgent(pos, initial = false) {
|
|
const group = new THREE.Group();
|
|
const colors = [0xFF1D6C, 0x2979FF, 0xF5A623, 0x9C27B0];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
// Body
|
|
const body = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(0.3, 0.4, 0.8, 8),
|
|
new THREE.MeshStandardMaterial({ color, metalness: 0.5, roughness: 0.3 })
|
|
);
|
|
body.position.y = 0.4;
|
|
group.add(body);
|
|
|
|
// Head
|
|
const head = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.3, 12, 12),
|
|
new THREE.MeshStandardMaterial({ color: 0xffffff, metalness: 0.3 })
|
|
);
|
|
head.position.y = 1;
|
|
group.add(head);
|
|
|
|
// Eye
|
|
const eye = new THREE.Mesh(
|
|
new THREE.CircleGeometry(0.15, 16),
|
|
new THREE.MeshBasicMaterial({ color: 0xFF1D6C })
|
|
);
|
|
eye.position.set(0, 1.05, 0.28);
|
|
group.add(eye);
|
|
|
|
// Antenna
|
|
const antenna = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(0.02, 0.02, 0.2, 6),
|
|
new THREE.MeshStandardMaterial({ color: 0x333333 })
|
|
);
|
|
antenna.position.set(0, 1.35, 0);
|
|
group.add(antenna);
|
|
|
|
const ball = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.06, 8, 8),
|
|
new THREE.MeshBasicMaterial({ color: 0xFF1D6C })
|
|
);
|
|
ball.position.set(0, 1.5, 0);
|
|
ball.userData = { glow: 0 };
|
|
group.add(ball);
|
|
|
|
// Hover ring
|
|
const ring = new THREE.Mesh(
|
|
new THREE.TorusGeometry(0.4, 0.04, 6, 24),
|
|
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 })
|
|
);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = -0.1;
|
|
group.add(ring);
|
|
|
|
const p = pos || getRandomPosition();
|
|
group.position.set(p.x, 0.8, p.z);
|
|
group.userData = {
|
|
type: 'agent',
|
|
targetX: p.x,
|
|
targetZ: p.z,
|
|
speed: 0.03 + Math.random() * 0.02,
|
|
hover: Math.random() * Math.PI * 2,
|
|
ball
|
|
};
|
|
|
|
agents.push(group);
|
|
scene.add(group);
|
|
|
|
if (!initial) {
|
|
GAME.counts.agents++;
|
|
updateQuest('spawn_agents', 1);
|
|
GAME.stats.magic = Math.min(100, GAME.stats.magic + 10);
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
function spawnBird() {
|
|
const group = new THREE.Group();
|
|
const colors = [0x4169E1, 0xFF6347, 0xFFD700, 0x32CD32];
|
|
const mat = new THREE.MeshStandardMaterial({ color: colors[Math.floor(Math.random() * colors.length)] });
|
|
|
|
const body = new THREE.Mesh(new THREE.SphereGeometry(0.2, 6, 6), mat);
|
|
body.scale.set(1, 0.7, 1.3);
|
|
group.add(body);
|
|
|
|
const wing = new THREE.Mesh(new THREE.BoxGeometry(1, 0.05, 0.3), mat);
|
|
wing.position.y = 0.05;
|
|
wing.userData = { flap: Math.random() * Math.PI * 2 };
|
|
group.add(wing);
|
|
|
|
const beak = new THREE.Mesh(
|
|
new THREE.ConeGeometry(0.05, 0.15, 4),
|
|
new THREE.MeshStandardMaterial({ color: 0xFFA500 })
|
|
);
|
|
beak.position.set(0, 0, 0.25);
|
|
beak.rotation.x = Math.PI / 2;
|
|
group.add(beak);
|
|
|
|
group.position.set(
|
|
(Math.random() - 0.5) * 100,
|
|
15 + Math.random() * 15,
|
|
(Math.random() - 0.5) * 100
|
|
);
|
|
group.userData = {
|
|
type: 'bird',
|
|
angle: Math.random() * Math.PI * 2,
|
|
radius: 15 + Math.random() * 30,
|
|
speed: 0.005 + Math.random() * 0.01,
|
|
wing
|
|
};
|
|
|
|
birds.push(group);
|
|
scene.add(group);
|
|
return group;
|
|
}
|
|
|
|
// ==================== BUILD SYSTEM ====================
|
|
const BUILD_COSTS = {
|
|
tree: { wood: 5 },
|
|
flower: { food: 2 },
|
|
house: { wood: 20, stone: 10 },
|
|
farm: { wood: 15, stone: 5 },
|
|
windmill: { wood: 30, stone: 20 },
|
|
agent: { gems: 50 },
|
|
portal: { gems: 100 },
|
|
castle: { wood: 200, stone: 150 }
|
|
};
|
|
|
|
function canAfford(type) {
|
|
const costs = BUILD_COSTS[type];
|
|
if (!costs) return false;
|
|
for (const [res, amount] of Object.entries(costs)) {
|
|
if (GAME.resources[res] < amount) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function spendResources(type) {
|
|
const costs = BUILD_COSTS[type];
|
|
for (const [res, amount] of Object.entries(costs)) {
|
|
GAME.resources[res] -= amount;
|
|
}
|
|
updateUI();
|
|
}
|
|
|
|
function build(type) {
|
|
if (!canAfford(type)) {
|
|
notify('⚠️ Not enough resources!', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Check level requirements
|
|
if (type === 'portal' && GAME.level < 5) {
|
|
notify('🔒 Unlock at Level 5', 'warning');
|
|
return;
|
|
}
|
|
if (type === 'castle' && GAME.level < 10) {
|
|
notify('🔒 Unlock at Level 10', 'warning');
|
|
return;
|
|
}
|
|
|
|
spendResources(type);
|
|
|
|
switch (type) {
|
|
case 'tree':
|
|
spawnTree();
|
|
notify('🌳 Tree planted!');
|
|
GAME.stats.nature = Math.min(100, GAME.stats.nature + 2);
|
|
addXP(5);
|
|
break;
|
|
case 'flower':
|
|
spawnFlower();
|
|
notify('🌸 Flower planted!');
|
|
GAME.stats.nature = Math.min(100, GAME.stats.nature + 1);
|
|
addXP(2);
|
|
break;
|
|
case 'house':
|
|
spawnHouse();
|
|
notify('🏠 House built!');
|
|
addXP(25);
|
|
break;
|
|
case 'farm':
|
|
notify('🌾 Farm built! +10 food/day');
|
|
addXP(20);
|
|
break;
|
|
case 'windmill':
|
|
notify('🏭 Windmill built! +5 energy/day');
|
|
addXP(30);
|
|
break;
|
|
case 'agent':
|
|
spawnAgent();
|
|
notify('🤖 Agent spawned!');
|
|
addXP(50);
|
|
break;
|
|
case 'portal':
|
|
notify('🌀 Portal opened!');
|
|
addXP(100);
|
|
break;
|
|
case 'castle':
|
|
notify('🏰 Castle constructed!');
|
|
addXP(200);
|
|
break;
|
|
}
|
|
|
|
updateCounts();
|
|
}
|
|
|
|
// ==================== PROGRESSION ====================
|
|
function addXP(amount) {
|
|
GAME.xp += amount;
|
|
|
|
while (GAME.xp >= GAME.xpNeeded) {
|
|
GAME.xp -= GAME.xpNeeded;
|
|
GAME.level++;
|
|
GAME.xpNeeded = Math.floor(GAME.xpNeeded * 1.5);
|
|
levelUp();
|
|
}
|
|
|
|
updateUI();
|
|
}
|
|
|
|
function levelUp() {
|
|
// Rewards
|
|
GAME.resources.wood += 50;
|
|
GAME.resources.stone += 30;
|
|
GAME.resources.gems += 10;
|
|
|
|
// Check unlocks
|
|
if (GAME.unlocks[GAME.level]) {
|
|
GAME.unlocks[GAME.level].forEach(item => {
|
|
document.querySelector(`[data-build="${item}"]`)?.classList.remove('locked');
|
|
notify(`🔓 Unlocked: ${item.toUpperCase()}!`);
|
|
});
|
|
}
|
|
|
|
// Show modal
|
|
document.getElementById('modalLevel').textContent = GAME.level;
|
|
document.getElementById('levelModal').classList.add('visible');
|
|
|
|
updateUI();
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('levelModal').classList.remove('visible');
|
|
}
|
|
|
|
function updateQuest(id, amount) {
|
|
const quest = GAME.quests.find(q => q.id === id);
|
|
if (!quest || quest.completed) return;
|
|
|
|
quest.progress += amount;
|
|
|
|
if (quest.progress >= quest.goal) {
|
|
quest.completed = true;
|
|
|
|
// Give rewards
|
|
if (quest.reward.xp) addXP(quest.reward.xp);
|
|
if (quest.reward.wood) GAME.resources.wood += quest.reward.wood;
|
|
if (quest.reward.stone) GAME.resources.stone += quest.reward.stone;
|
|
if (quest.reward.food) GAME.resources.food += quest.reward.food;
|
|
if (quest.reward.gems) GAME.resources.gems += quest.reward.gems;
|
|
if (quest.reward.magic) GAME.stats.magic = Math.min(100, GAME.stats.magic + quest.reward.magic);
|
|
|
|
notify(`🎉 Quest Complete: ${quest.name}!`);
|
|
updateUI();
|
|
}
|
|
|
|
renderQuests();
|
|
}
|
|
|
|
// ==================== TIME & WEATHER ====================
|
|
function updateTime() {
|
|
GAME.minute += 0.5 * GAME.speed;
|
|
if (GAME.minute >= 60) {
|
|
GAME.minute = 0;
|
|
GAME.hour++;
|
|
|
|
// Resource generation
|
|
GAME.resources.food = Math.min(999, GAME.resources.food + 1);
|
|
GAME.resources.wood = Math.min(999, GAME.resources.wood + Math.floor(GAME.counts.trees / 20));
|
|
GAME.resources.energy = Math.min(100, GAME.resources.energy + 1);
|
|
|
|
if (GAME.hour >= 24) {
|
|
GAME.hour = 0;
|
|
GAME.day++;
|
|
notify(`☀️ Day ${GAME.day} begins!`);
|
|
|
|
// Daily bonuses
|
|
GAME.resources.gems += 1;
|
|
}
|
|
|
|
// Night check
|
|
if (GAME.hour === 20) {
|
|
setNight(true);
|
|
updateQuest('survive_night', 1);
|
|
} else if (GAME.hour === 6) {
|
|
setNight(false);
|
|
}
|
|
}
|
|
|
|
updateUI();
|
|
}
|
|
|
|
function setNight(night) {
|
|
isNight = night;
|
|
|
|
if (night) {
|
|
scene.background = new THREE.Color(COLORS.skyNight);
|
|
scene.fog.color = new THREE.Color(COLORS.skyNight);
|
|
window.sunLight.intensity = 0.1;
|
|
window.hemiLight.intensity = 0.1;
|
|
|
|
// Light up windows
|
|
houses.forEach(h => {
|
|
if (h.userData.windows) {
|
|
h.userData.windows.emissive.setHex(0xFFAA00);
|
|
h.userData.windows.emissiveIntensity = 0.5;
|
|
}
|
|
});
|
|
} else {
|
|
scene.background = new THREE.Color(COLORS.sky);
|
|
scene.fog.color = new THREE.Color(COLORS.sky);
|
|
window.sunLight.intensity = 1;
|
|
window.hemiLight.intensity = 0.3;
|
|
|
|
houses.forEach(h => {
|
|
if (h.userData.windows) {
|
|
h.userData.windows.emissive.setHex(0x000000);
|
|
}
|
|
});
|
|
}
|
|
|
|
document.getElementById('btnDay').classList.toggle('active', !night);
|
|
document.getElementById('btnNight').classList.toggle('active', night);
|
|
}
|
|
|
|
function toggleRain() {
|
|
isRaining = !isRaining;
|
|
rain.visible = isRaining;
|
|
document.getElementById('btnRain').classList.toggle('active', isRaining);
|
|
|
|
if (isRaining) {
|
|
notify('🌧️ It started raining!');
|
|
GAME.stats.nature = Math.min(100, GAME.stats.nature + 5);
|
|
}
|
|
}
|
|
|
|
// ==================== UI ====================
|
|
function updateUI() {
|
|
// Resources
|
|
document.getElementById('resWood').textContent = GAME.resources.wood;
|
|
document.getElementById('resStone').textContent = GAME.resources.stone;
|
|
document.getElementById('resFood').textContent = GAME.resources.food;
|
|
document.getElementById('resGems').textContent = GAME.resources.gems;
|
|
document.getElementById('resEnergy').textContent = GAME.resources.energy;
|
|
|
|
// Level
|
|
document.getElementById('levelBadge').textContent = GAME.level;
|
|
document.getElementById('levelNum').textContent = GAME.level;
|
|
document.getElementById('xpFill').style.width = (GAME.xp / GAME.xpNeeded * 100) + '%';
|
|
|
|
// Time
|
|
const h = GAME.hour.toString().padStart(2, '0');
|
|
const m = Math.floor(GAME.minute).toString().padStart(2, '0');
|
|
document.getElementById('timeText').textContent = `${h}:${m}`;
|
|
document.getElementById('dayNum').textContent = GAME.day;
|
|
document.getElementById('weatherIcon').textContent = isRaining ? '🌧️' : (isNight ? '🌙' : '☀️');
|
|
|
|
// Stats
|
|
['happiness', 'nature', 'energy', 'magic'].forEach(stat => {
|
|
const val = GAME.stats[stat];
|
|
document.getElementById(`stat${stat.charAt(0).toUpperCase() + stat.slice(1)}`).style.width = val + '%';
|
|
document.getElementById(`val${stat.charAt(0).toUpperCase() + stat.slice(1)}`).textContent = Math.floor(val) + '%';
|
|
});
|
|
}
|
|
|
|
function updateCounts() {
|
|
GAME.counts.trees = trees.length;
|
|
GAME.counts.flowers = flowers.length;
|
|
GAME.counts.houses = houses.length;
|
|
GAME.counts.agents = agents.length;
|
|
GAME.counts.animals = animals.length;
|
|
}
|
|
|
|
function renderQuests() {
|
|
const container = document.getElementById('questList');
|
|
container.innerHTML = '';
|
|
|
|
GAME.quests.slice(0, 4).forEach(quest => {
|
|
const el = document.createElement('div');
|
|
el.className = 'quest-item' + (quest.completed ? ' completed' : '');
|
|
el.innerHTML = `
|
|
<div class="quest-name">${quest.icon} ${quest.name}</div>
|
|
<div class="quest-desc">${quest.desc}</div>
|
|
<div class="quest-progress">
|
|
<div class="quest-progress-fill" style="width: ${Math.min(100, quest.progress / quest.goal * 100)}%"></div>
|
|
</div>
|
|
<div class="quest-reward">Reward: +${quest.reward.xp} XP ${quest.reward.gems ? `+${quest.reward.gems}💎` : ''}</div>
|
|
`;
|
|
container.appendChild(el);
|
|
});
|
|
}
|
|
|
|
function renderInventory() {
|
|
const grid = document.getElementById('inventoryGrid');
|
|
grid.innerHTML = '';
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const item = GAME.inventory[i];
|
|
const slot = document.createElement('div');
|
|
slot.className = 'inv-slot' + (selectedSlot === i ? ' selected' : '');
|
|
|
|
if (item) {
|
|
slot.innerHTML = `
|
|
<span class="inv-slot-icon">${item.icon}</span>
|
|
${item.count > 1 ? `<span class="inv-slot-count">${item.count}</span>` : ''}
|
|
`;
|
|
slot.onclick = () => {
|
|
selectedSlot = selectedSlot === i ? null : i;
|
|
renderInventory();
|
|
};
|
|
}
|
|
|
|
grid.appendChild(slot);
|
|
}
|
|
}
|
|
|
|
function notify(message, type = 'success') {
|
|
const container = document.getElementById('notifications');
|
|
const el = document.createElement('div');
|
|
el.className = 'notification ' + type;
|
|
el.textContent = message;
|
|
container.appendChild(el);
|
|
|
|
setTimeout(() => el.remove(), 3000);
|
|
}
|
|
|
|
// ==================== EVENTS ====================
|
|
function setupEvents() {
|
|
// Resize
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
// Mouse
|
|
renderer.domElement.addEventListener('mousedown', e => {
|
|
isDragging = true;
|
|
prevMouse = { x: e.clientX, y: e.clientY };
|
|
});
|
|
renderer.domElement.addEventListener('mousemove', e => {
|
|
if (!isDragging) return;
|
|
const dx = e.clientX - prevMouse.x;
|
|
const dy = e.clientY - prevMouse.y;
|
|
camAngle += dx * 0.005;
|
|
camHeight = Math.max(20, Math.min(100, camHeight - dy * 0.3));
|
|
updateCamera();
|
|
prevMouse = { x: e.clientX, y: e.clientY };
|
|
});
|
|
renderer.domElement.addEventListener('mouseup', () => isDragging = false);
|
|
renderer.domElement.addEventListener('wheel', e => {
|
|
camDist = Math.max(50, Math.min(200, camDist + e.deltaY * 0.1));
|
|
updateCamera();
|
|
});
|
|
|
|
// Touch
|
|
renderer.domElement.addEventListener('touchstart', e => {
|
|
isDragging = true;
|
|
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
}, { passive: true });
|
|
renderer.domElement.addEventListener('touchmove', e => {
|
|
if (!isDragging) return;
|
|
const dx = e.touches[0].clientX - prevMouse.x;
|
|
const dy = e.touches[0].clientY - prevMouse.y;
|
|
camAngle += dx * 0.005;
|
|
camHeight = Math.max(20, Math.min(100, camHeight - dy * 0.3));
|
|
updateCamera();
|
|
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
}, { passive: true });
|
|
renderer.domElement.addEventListener('touchend', () => isDragging = false);
|
|
|
|
// Keyboard
|
|
document.addEventListener('keydown', e => {
|
|
const num = parseInt(e.key);
|
|
if (num >= 1 && num <= 8) {
|
|
const types = ['tree', 'flower', 'house', 'farm', 'windmill', 'agent', 'portal', 'castle'];
|
|
build(types[num - 1]);
|
|
}
|
|
});
|
|
|
|
// Build buttons
|
|
document.querySelectorAll('.build-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const type = btn.dataset.build;
|
|
if (!btn.classList.contains('locked')) {
|
|
build(type);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Control buttons
|
|
document.getElementById('btnRotate').addEventListener('click', function() {
|
|
autoRotate = !autoRotate;
|
|
this.classList.toggle('active', autoRotate);
|
|
});
|
|
document.getElementById('btnDay').addEventListener('click', () => setNight(false));
|
|
document.getElementById('btnNight').addEventListener('click', () => setNight(true));
|
|
document.getElementById('btnRain').addEventListener('click', toggleRain);
|
|
document.getElementById('btnSpeed').addEventListener('click', function() {
|
|
GAME.speed = GAME.speed === 1 ? 3 : 1;
|
|
this.classList.toggle('active', GAME.speed > 1);
|
|
notify(GAME.speed > 1 ? '⏩ Speed x3' : '▶️ Normal speed');
|
|
});
|
|
}
|
|
|
|
function updateCamera() {
|
|
camera.position.x = Math.sin(camAngle) * camDist;
|
|
camera.position.z = Math.cos(camAngle) * camDist;
|
|
camera.position.y = camHeight;
|
|
camera.lookAt(0, 0, 0);
|
|
}
|
|
|
|
// ==================== ANIMATION ====================
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
time += 0.016;
|
|
|
|
// Auto rotate
|
|
if (autoRotate && !isDragging) {
|
|
camAngle += 0.002;
|
|
updateCamera();
|
|
}
|
|
|
|
// Time
|
|
updateTime();
|
|
|
|
// Trees sway
|
|
trees.forEach(t => {
|
|
t.userData.sway += 0.015;
|
|
t.rotation.z = Math.sin(t.userData.sway) * 0.02;
|
|
});
|
|
|
|
// Flowers sway
|
|
flowers.forEach(f => {
|
|
f.userData.sway += 0.02;
|
|
f.rotation.z = Math.sin(f.userData.sway) * 0.04;
|
|
});
|
|
|
|
// Animals
|
|
animals.forEach(a => {
|
|
const d = a.userData;
|
|
d.idle += 0.016;
|
|
|
|
if (d.idle > 2) {
|
|
d.idle = 0;
|
|
const angle = Math.random() * Math.PI * 2;
|
|
d.targetX = Math.cos(angle) * (10 + Math.random() * 40);
|
|
d.targetZ = Math.sin(angle) * (10 + Math.random() * 40);
|
|
}
|
|
|
|
const dx = d.targetX - a.position.x;
|
|
const dz = d.targetZ - a.position.z;
|
|
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
|
|
if (dist > 0.5) {
|
|
a.position.x += (dx / dist) * d.speed;
|
|
a.position.z += (dz / dist) * d.speed;
|
|
a.rotation.y = Math.atan2(dx, dz);
|
|
d.hop += 0.15;
|
|
a.position.y = 0.4 + Math.abs(Math.sin(d.hop)) * 0.2;
|
|
}
|
|
});
|
|
|
|
// Agents
|
|
agents.forEach(ag => {
|
|
const d = ag.userData;
|
|
d.hover += 0.04;
|
|
ag.position.y = 0.8 + Math.sin(d.hover) * 0.15;
|
|
|
|
if (Math.random() < 0.005) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
d.targetX = Math.cos(angle) * (5 + Math.random() * 40);
|
|
d.targetZ = Math.sin(angle) * (5 + Math.random() * 40);
|
|
}
|
|
|
|
const dx = d.targetX - ag.position.x;
|
|
const dz = d.targetZ - ag.position.z;
|
|
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
|
|
if (dist > 1) {
|
|
ag.position.x += (dx / dist) * d.speed;
|
|
ag.position.z += (dz / dist) * d.speed;
|
|
ag.rotation.y = Math.atan2(dx, dz);
|
|
}
|
|
|
|
// Antenna glow
|
|
if (d.ball) {
|
|
d.ball.userData.glow += 0.1;
|
|
const h = (Math.sin(d.ball.userData.glow) + 1) * 0.15;
|
|
d.ball.material.color.setHSL(h, 1, 0.5);
|
|
}
|
|
});
|
|
|
|
// Birds
|
|
birds.forEach(b => {
|
|
const d = b.userData;
|
|
d.angle += d.speed;
|
|
b.position.x = Math.cos(d.angle) * d.radius;
|
|
b.position.z = Math.sin(d.angle) * d.radius;
|
|
b.rotation.y = d.angle + Math.PI / 2;
|
|
|
|
if (d.wing) {
|
|
d.wing.userData.flap += 0.25;
|
|
d.wing.rotation.z = Math.sin(d.wing.userData.flap) * 0.4;
|
|
}
|
|
});
|
|
|
|
// Clouds
|
|
clouds.forEach(c => {
|
|
c.position.x += c.userData.speed;
|
|
if (c.position.x > 100) c.position.x = -100;
|
|
});
|
|
|
|
// Rain
|
|
if (rain.visible) {
|
|
const pos = rain.geometry.attributes.position.array;
|
|
const vel = rain.userData.velocities;
|
|
for (let i = 0; i < pos.length / 3; i++) {
|
|
pos[i * 3 + 1] -= vel[i];
|
|
if (pos[i * 3 + 1] < 0) {
|
|
pos[i * 3 + 1] = 80;
|
|
pos[i * 3] = (Math.random() - 0.5) * 150;
|
|
pos[i * 3 + 2] = (Math.random() - 0.5) * 150;
|
|
}
|
|
}
|
|
rain.geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
|
|
// Water wave
|
|
water.position.y = -2 + Math.sin(time * 0.5) * 0.1;
|
|
|
|
// Stats decay
|
|
GAME.stats.happiness = Math.max(0, GAME.stats.happiness - 0.001);
|
|
GAME.stats.energy = Math.max(0, GAME.stats.energy - 0.002);
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// Start
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|