Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Greenhouse 3D: Interactive Grow Room</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
:root {
--phone-bg: #0b120c;
--phone-neon: #39ff14;
--danger: #ff3333;
}
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background-color: #050705;
font-family: 'Segoe UI', Roboto, sans-serif;
user-select: none; -webkit-user-select: none;
}
#canvas-3d {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1;
}
/* HUD сверху */
#hud {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
z-index: 10; display: flex; gap: 20px;
background: rgba(10, 15, 10, 0.85); padding: 12px 30px;
border-radius: 30px; border: 1px solid rgba(57, 255, 20, 0.2);
backdrop-filter: blur(8px); pointer-events: none;
}
.hud-item { text-align: center; color: #fff; min-width: 80px; }
.hud-label { font-size: 10px; color: #666; text-transform: uppercase; font-weight: bold; }
.hud-val { font-size: 16px; font-weight: bold; font-family: monospace; }
/* Сигнальное табло */
#action-toast {
position: absolute; top: 90px; left: 50%; transform: translateX(-50%);
z-index: 10; background: rgba(211, 47, 47, 0.9); color: white;
padding: 8px 20px; border-radius: 8px; font-size: 13px; font-weight: bold;
display: none; box-shadow: 0 0 15px rgba(255,0,0,0.4);
}
/* Кнопка "Достать телефон" */
#phone-toggle-btn {
position: absolute; bottom: 25px; right: 25px; z-index: 15;
background: linear-gradient(135deg, #142312, #070d07);
color: var(--phone-neon); border: 2px solid var(--phone-neon);
padding: 15px 25px; font-size: 14px; font-weight: bold; border-radius: 12px;
cursor: pointer; box-shadow: 0 0 20px rgba(57,255,20,0.15);
text-transform: uppercase; letter-spacing: 1px; transition: all 0.3s;
}
#phone-toggle-btn:hover { background: var(--phone-neon); color: #000; box-shadow: 0 0 30px var(--phone-neon); }
/* Экран Смартфона */
#smartphone {
position: absolute; bottom: -650px; right: 25px; width: 330px; height: 560px;
background-color: var(--phone-bg); border-radius: 32px; z-index: 20;
border: 4px solid #142012; box-shadow: 0 25px 60px rgba(0,0,0,0.8);
transition: bottom 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex; flex-direction: column; overflow: hidden;
}
#smartphone.active { bottom: 95px; }
.phone-header {
background: #0f1a0f; padding: 15px; text-align: center;
border-bottom: 1px solid #1d331c; color: var(--phone-neon);
font-family: monospace; font-size: 14px; font-weight: bold;
}
.phone-content { padding: 15px; flex-grow: 1; overflow-y: auto; color: #cbd5e1; }
.app-card { background: #132212; border: 1px solid #1d331c; padding: 12px; border-radius: 10px; margin-bottom: 12px; }
.app-btn {
width: 100%; background: var(--phone-neon); color: #000; border: none;
padding: 10px; font-weight: bold; border-radius: 6px; cursor: pointer;
margin-top: 8px; text-transform: uppercase; font-size: 11px;
}
.app-btn.bribe { background: #ff5252; color: white; }
.phone-input {
width: 80%; background: #070d07; border: 1px solid #1d331c; color: #fff;
padding: 6px; text-align: center; border-radius: 4px; font-weight: bold; margin-top: 5px;
}
#phone-log {
background: #050a05; border-radius: 6px; padding: 8px;
font-family: monospace; font-size: 10px; color: #39ff14; height: 70px; overflow-y: auto;
}
/* Инструкция */
#table-instructions {
position: absolute; bottom: 25px; left: 25px; z-index: 5;
color: #5d7a5b; font-size: 12px; font-family: monospace; line-height: 1.6;
background: rgba(0,0,0,0.5); padding: 10px; border-radius: 8px; pointer-events: none;
}
</style>
</head>
<body>
<div id="canvas-3d"></div>
<div id="hud">
<div class="hud-item"><div class="hud-label">Бюджет ($)</div><div id="hud-money" class="hud-val">15,000</div></div>
<div class="hud-item"><div class="hud-label">Урожай (кг)</div><div id="hud-stock" class="hud-val">0.0</div></div>
<div class="hud-item"><div class="hud-label">Семена</div><div id="hud-seeds" class="hud-val">5 шт</div></div>
<div class="hud-item"><div class="hud-label">Улики</div><div id="hud-wanted" class="hud-val" style="color:var(--danger)">0%</div></div>
</div>
<div id="action-toast">ВЛАЖНОСТЬ КОРНЕЙ КРИТИЧЕСКАЯ! ОТКАЧАЙ ВОДУ СИНИМ ВЕНТИЛЕМ!</div>
<div id="table-instructions">
ТЕХНОЛОГИЧЕСКИЙ ГРОУБОКС:<br>
1. Перетащи МЫШКОЙ Зеленый Мешок с семенами в Горшок, чтобы посадить куст.<br>
2. Куст начнет расти в 3D. Перетащи Желтую Канистру с удобрением для буста соцветий.<br>
3. Кликай на Синий Круглый Вентиль помпы справа, чтобы вовремя снижать влажность почвы.
</div>
<button id="phone-toggle-btn" onclick="togglePhone()">Открыть телефон</button>
<div id="smartphone">
<div class="phone-header">GREEN_NET OS v4.1</div>
<div class="phone-content">
<div class="app-card">
<div style="font-weight:bold; color:var(--phone-neon); font-size:13px; margin-bottom:5px;">GREEN_MARKET // ОПТ</div>
<div style="font-size:11px;">Склад готового продукта: <strong id="p-weight" style="color:#fff">0.0</strong> кг</div>
<div id="p-client-info" style="font-size:11px; color:#aaa; margin: 5px 0; font-style:italic;">"Ожидание созревания куста..."</div>
<div style="margin-top: 8px; font-size:11px;">
Цена за 1 кг ($): <br>
<input type="number" id="p-offer-price" class="phone-input" value="6000">
</div>
<button class="app-btn" onclick="phoneSellBatch()">Продать партию</button>
</div>
<div class="app-card">
<div style="font-weight:bold; color:#ffb300; font-size:13px; margin-bottom:5px;">ПРИКРЫТИЕ И СНАБЖЕНИЕ</div>
<button class="app-btn" style="background:#ffb300;" onclick="phoneBuySupplies()">Заказать семена элит (+5шт) (-$3k)</button>
<button class="app-btn bribe" onclick="phoneBribePolice()">Дать взятку шерифу (-$2k)</button>
</div>
<div id="phone-log">Вход в сеть выполнен. Прокси-сервер: Амстердам.</div>
</div>
</div>
<script>
// Баланс игры
let state = {
money: 15000, seeds: 5, wanted: 0, stock: 0, purity: 0, // purity тут выступает как ТГК / качество
isCooking: false, progress: 0, temp: 24, pressure: 50, // pressure тут выступает как влажность грунта
catalystAdded: false, loop: null
};
// 3D Графика
let scene, camera, renderer;
let pot, plant, seedBag, fertilizerCan, valve, gaugeHand;
let raycaster, mouse, dragObject = null, isDragging = false;
let plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
function init3DGame() {
const container = document.getElementById('canvas-3d');
scene = new THREE.Scene();
scene.background = new THREE.Color(0x050805);
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 7, 12);
camera.lookAt(0, 1, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// Освещение гроубокса
let ambient = new THREE.AmbientLight(0x112211);
scene.add(ambient);
// Фитолампа (Ультрафиолетовый / Пурпурный свет)
let uvLight = new THREE.PointLight(0xdc26ff, 2.5, 25);
uvLight.position.set(0, 6, 0);
scene.add(uvLight);
let daylight = new THREE.PointLight(0xffffff, 0.8, 30);
daylight.position.set(3, 8, 3);
scene.add(daylight);
// Стол
let tableGeo = new THREE.BoxGeometry(16, 0.2, 8);
let tableMat = new THREE.MeshStandardMaterial({ color: 0x141a13, roughness: 0.8 });
let table = new THREE.Mesh(tableGeo, tableMat);
table.position.y = -0.1;
scene.add(table);
// ОБЪЕКТЫ
// 1. Горшок с землей по центру
pot = new THREE.Group();
let potGeo = new THREE.CylinderGeometry(1.6, 1.1, 1.8, 24);
let potMat = new THREE.MeshStandardMaterial({ color: 0x2b1e17, roughness: 0.9 });
let potMesh = new THREE.Mesh(potGeo, potMat);
let soilGeo = new THREE.CylinderGeometry(1.5, 1.4, 0.2, 24);
let soilMat = new THREE.MeshStandardMaterial({ color: 0x140d0a, roughness: 1.0 });
let soilMesh = new THREE.Mesh(soilGeo, soilMat);
soilMesh.position.y = 0.8;
pot.add(potMesh); pot.add(soilMesh);
pot.position.set(0, 0.9, 0);
scene.add(pot);
// 3D Растение (Модель, которая будет расти физически)
plant = new THREE.Group();
plant.position.set(0, 1, 0);
scene.add(plant);
// 2. Мешок семян (Перетаскиваемый объект слева)
let bagGroup = new THREE.Group();
let bagGeo = new THREE.BoxGeometry(1.1, 1.5, 0.7);
let bagMat = new THREE.MeshStandardMaterial({ color: 0x2e7d32, roughness: 0.5 }); // Зеленый мешок
let bagMesh = new THREE.Mesh(bagGeo, bagMat);
bagGroup.add(bagMesh);
bagGroup.position.set(-4.5, 0.75, 1);
bagGroup.userData = { type: "seeds", originX: -4.5, originZ: 1 };
seedBag = bagGroup;
scene.add(seedBag);
// 3. Удобрения (Желтая канистра справа впереди)
let fertGroup = new THREE.Group();
let fertGeo = new THREE.CylinderGeometry(0.5, 0.5, 1.4, 16);
let fertMat = new THREE.MeshStandardMaterial({ color: 0xfbc02d, roughness: 0.2 }); // Желтая бутылка
let fertMesh = new THREE.Mesh(fertGeo, fertMat);
fertGroup.add(fertMesh);
fertGroup.position.set(-2.5, 0.7, 2);
fertGroup.userData = { type: "fertilizer", originX: -2.5, originZ: 2 };
fertilizerCan = fertGroup;
scene.add(fertilizerCan);
// 4. Гигрометр (Прибор контроля влажности почвы)
let gauge = new THREE.Group();
let backGeo = new THREE.CylinderGeometry(0.9, 0.9, 0.3, 32);
backGeo.rotateX(Math.PI / 2);
let gaugeBack = new THREE.Mesh(backGeo, new THREE.MeshStandardMaterial({ color: 0x223021 }));
let arrowGeo = new THREE.BoxGeometry(0.7, 0.08, 0.05);
arrowGeo.translate(0.35, 0, 0);
gaugeHand = new THREE.Mesh(arrowGeo, new THREE.MeshBasicMaterial({ color: 0x00e5ff }));
gaugeHand.position.z = 0.18;
gauge.add(gaugeBack); gauge.add(gaugeHand);
gauge.position.set(4, 1.2, -1);
scene.add(gauge);
// Синий вентиль гидропонной помпы (Для кликов)
let valveGroup = new THREE.Group();
let wheelGeo = new THREE.TorusGeometry(0.6, 0.15, 8, 24);
wheelGeo.rotateX(Math.PI / 2);
let wheel = new THREE.Mesh(wheelGeo, new THREE.MeshStandardMaterial({ color: 0x0288d1, roughness: 0.4 }));
let coreGeo = new THREE.CylinderGeometry(0.15, 0.15, 0.6, 8);
let core = new THREE.Mesh(coreGeo, new THREE.MeshStandardMaterial({ color: 0x777 }));
core.position.y = -0.2;
valveGroup.add(wheel); valveGroup.add(core);
valveGroup.position.set(3.5, 0.4, 1.5);
valveGroup.userData = { type: "valve" };
valve = valveGroup;
scene.add(valve);
// Обработчики мыши
window.addEventListener('mousedown', onPointerDown);
window.addEventListener('mousemove', onPointerMove);
window.addEventListener('mouseup', onPointerUp);
window.addEventListener('resize', onWindowResize);
animateLab();
}
// Рендеринг веток куста в 3D
function rebuildPlant3D(progress) {
// Очищаем старые листья
while(plant.children.length > 0){
plant.remove(plant.children[0]);
}
if (progress <= 0) return;
// Создаем новые ветки на основе прогресса роста
let height = (progress / 100) * 2.5;
let stemGeo = new THREE.CylinderGeometry(0.05, 0.15, height, 8);
stemGeo.translate(0, height/2, 0);
let stemMat = new THREE.MeshStandardMaterial({ color: 0x388e3c, roughness: 0.9 });
let stem = new THREE.Mesh(stemGeo, stemMat);
plant.add(stem);
// Листья/Соцветия (Зеленые сферы)
let leafCount = Math.floor(progress / 15) + 1;
let leafMat = new THREE.MeshStandardMaterial({
color: state.catalystAdded ? 0x1b5e20 : 0x4caf50,
roughness: 0.6
});
for(let i=0; i<leafCount; i++) {
let size = 0.2 + (i * 0.08);
let leafGeo = new THREE.SphereGeometry(size, 8, 8);
let leaf = new THREE.Mesh(leafGeo, leafMat);
leaf.position.set(
Math.sin(i * 2) * 0.4,
(height * (i / leafCount)) + 0.2,
Math.cos(i * 2) * 0.4
);
plant.add(leaf);
}
}
// --- DRAG & DROP В 3D ---
function onPointerDown(e) {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
let intersects = raycaster.intersectObjects([seedBag, fertilizerCan, valve], true);
if (intersects.length > 0) {
let obj = intersects[0].object;
while (obj.parent && !obj.userData.type) { obj = obj.parent; }
if (obj.userData.type === "valve") {
valve.rotation.y += 0.8;
reduceMoisture();
} else if (obj.userData.type === "seeds" || obj.userData.type === "fertilizer") {
isDragging = true;
dragObject = obj;
}
}
}
function onPointerMove(e) {
if (!isDragging || !dragObject) return;
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
let raySpace = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, raySpace);
dragObject.position.x = raySpace.x;
dragObject.position.z = raySpace.z;
dragObject.position.y = (dragObject.userData.type === "seeds") ? 1.4 : 1.2;
// Проверка сближения с горшком (0, 0)
let dist = Math.sqrt(dragObject.position.x * dragObject.position.x + dragObject.position.z * dragObject.position.z);
if (dist < 1.4) {
if (dragObject.userData.type === "seeds" && !state.isCooking) {
startPlantGrowth();
returnToOrigin(dragObject);
} else if (dragObject.userData.type === "fertilizer" && state.isCooking && !state.catalystAdded) {
addFertilizer();
returnToOrigin(dragObject);
}
}
}
function onPointerUp() {
if (dragObject) returnToOrigin(dragObject);
isDragging = false; dragObject = null;
}
function returnToOrigin(obj) {
obj.position.x = obj.userData.originX;
obj.position.z = obj.userData.originZ;
obj.position.y = 0.7;
}
// --- СИСТЕМНАЯ ЛОГИКА КУЛЬТИВАЦИИ ---
function startPlantGrowth() {
if (state.seeds < 1) { phoneLog("Маркет: У вас кончились семена сортовых культур!"); return; }
if (state.stock > 0) { phoneLog("Внимание: На складе лежит старый урожай, продайте его!"); return; }
state.seeds--; state.isCooking = true; state.progress = 0; state.pressure = 40; state.purity = 70;
state.catalystAdded = false;
isDragging = false; dragObject = null;
updateUIScreen();
state.loop = setInterval(() => {
state.progress += 2.5;
state.pressure += Math.random() * 9.5 - 3.8; // Рост влажности субстрата
// Движение стрелки прибора
let radAngle = ((state.pressure / 100) * Math.PI) - (Math.PI / 2);
gaugeHand.rotation.z = -radAngle;
// Перестройка 3D модели дерева в реальном времени
rebuildPlant3D(state.progress);
// Проверка критических порогов влажности
let toast = document.getElementById('action-toast');
if (state.pressure > 70 || state.pressure < 30) {
state.purity -= 0.6; // Качество падает
toast.style.display = 'block';
} else {
toast.style.display = 'none';
}
// Гибель урожая
if (state.pressure >= 100 || state.pressure <= 0) {
toast.style.display = 'none';
phoneLog("Система: Корневая система погибла из-за дисбаланса влаги!");
rebuildPlant3D(0);
stopLabLoop();
return;
}
if (state.progress >= 100) finishPlantGrowth();
updateUIScreen();
}, 350);
}
function addFertilizer() {
state.catalystAdded = true;
isDragging = false; dragObject = null;
if (state.pressure >= 35 && state.pressure <= 65) {
state.purity += 18.5; // Значительный буст качества ТГК
phoneLog("Гроубокс: Стимулятор цветения усвоен на 100%.");
} else {
state.purity += 5;
phoneLog("Гроубокс: Избыток минералов обжег соцветия куста.");
}
updateUIScreen();
}
function reduceMoisture() {
if (!state.isCooking) return;
state.pressure -= 22; // Помпа откачивает воду
if (state.pressure < 5) state.pressure = 5;
}
function stopLabLoop() {
clearInterval(state.loop); state.isCooking = false;
document.getElementById('action-toast').style.display = 'none';
let radAngle = ((50 / 100) * Math.PI) - (Math.PI / 2);
gaugeHand.rotation.z = -radAngle;
updateUIScreen();
}
function finishPlantGrowth() {
stopLabLoop();
state.stock = 1.5 + (state.purity / 35);
phoneLog(`Маркет: Собрано готовых сухих соцветий: ${state.stock.toFixed(2)} кг. Мощность: ${state.purity.toFixed(1)}%`);
let clients = ["Клуб Amsterdam", "Кофишоп Barney", "Местный дилер"];
document.getElementById('p-client-info').innerText = `Заказчик: ${clients[Math.floor(Math.random()*clients.length)]} (Требуется качество: ${state.purity.toFixed(0)}%)`;
updateUIScreen();
}
// --- УПРАВЛЕНИЕ СМАРТФОНОМ ---
function togglePhone() {
document.getElementById('smartphone').classList.toggle('active');
}
function phoneSellBatch() {
if (state.stock <= 0) { phoneLog("Ошибка: Склад пуст!"); return; }
let offer = parseFloat(document.getElementById('p-offer-price').value);
let maxLimitPrice = 5000 * (state.purity / 60);
if (offer <= maxLimitPrice) {
let win = state.stock * offer;
state.money += win;
state.wanted += Math.random() * 9 + 3;
phoneLog(`Криптосеть: Товар принят. На счет переведено +$${Math.round(win)}`);
state.stock = 0;
rebuildPlant3D(0); // Срезаем куст в горшке
document.getElementById('p-client-info').innerText = "Ожидание нового урожая...";
} else {
phoneLog("Отказ: Диспетчер отклонил цену. Слишком дорого для этой селекции.");
state.wanted += 2;
}
updateUIScreen();
}
function phoneBuySupplies() {
if (state.money < 3000) { phoneLog("Ошибка счета: Баланс заблокирован!"); return; }
state.money -= 3000; state.seeds += 5;
phoneLog("Поставщик: Тайник с 5 семенами заложен за гаражами.");
updateUIScreen();
}
function phoneBribePolice() {
if (state.money < 2000) { phoneLog("Адвокат: Мало денег для взятки."); return; }
state.money -= 2000; state.wanted = Math.max(0, state.wanted - 30);
phoneLog("Информатор: Патрульные маршруты изменены, улики утеряны.");
updateUIScreen();
}
function phoneLog(msg) {
let l = document.getElementById('phone-log');
l.innerHTML += `<br>» ${msg}`; l.scrollTop = l.scrollHeight;
}
// --- СЕРВИСНЫЕ ИНТЕРФЕЙСЫ ---
function updateUIScreen() {
document.getElementById('hud-money').innerText = state.money.toLocaleString();
document.getElementById('hud-stock').innerText = state.stock.toFixed(2);
document.getElementById('hud-seeds').innerText = state.seeds + ' шт';
document.getElementById('hud-wanted').innerText = Math.round(state.wanted) + '%';
document.getElementById('p-weight').innerText = state.stock.toFixed(2);
if (state.wanted >= 100) {
alert("ИГРА ОКОНЧЕНА. Полиция вскрыла двери подвала и конфисковала гидропонную оранжерею!");
location.reload();
}
}
function animateLab() {
requestAnimationFrame(animateLab);
// Небольшая плавная анимация покачивания куста
if(state.isCooking && plant) {
plant.rotation.y = Math.sin(Date.now() * 0.001) * 0.08;
}
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.onload = () => {
init3DGame();
updateUIScreen();
};
</script>
</body>
</html>