export class UI { constructor(game) { this.game = game; this.dialogCallback = null; this.inventoryOpen = false; this.questsOpen = false; this.skillsOpen = false; this.achievementsOpen = false; this.minimapCtx = null; this.begProgressEl = null; this.buskProgressEl = null; this.tooltipEl = null; } init() { document.getElementById('hud').classList.remove('hidden'); // Close buttons document.getElementById('btn-close-inv').addEventListener('click', () => this.toggleInventory()); document.getElementById('btn-close-quest').addEventListener('click', () => this.toggleQuests()); document.getElementById('btn-close-skills').addEventListener('click', () => this.toggleSkills()); document.getElementById('btn-close-achievements').addEventListener('click', () => this.toggleAchievements()); // Hotbar clicks const hotbarItems = document.querySelectorAll('.hotbar-item'); if (hotbarItems[0]) hotbarItems[0].addEventListener('click', () => this.toggleInventory()); if (hotbarItems[1]) hotbarItems[1].addEventListener('click', () => this.toggleQuests()); if (hotbarItems[2]) hotbarItems[2].addEventListener('click', () => this.toggleSkills()); if (hotbarItems[3]) hotbarItems[3].addEventListener('click', () => this.toggleAchievements()); // Minimap const minimap = document.getElementById('minimap'); this.minimapCtx = minimap.getContext('2d'); // Tooltip this.tooltipEl = document.getElementById('tooltip'); // Beg progress bar this.createBegProgress(); this.createBuskProgress(); } createBegProgress() { const el = document.createElement('div'); el.id = 'beg-progress'; el.classList.add('hidden'); el.innerHTML = `
Попрошайничество...
`; document.body.appendChild(el); this.begProgressEl = el; } createBuskProgress() { const el = document.createElement('div'); el.id = 'busk-progress'; el.classList.add('hidden'); el.innerHTML = `
Играю на гармошке...
`; document.body.appendChild(el); this.buskProgressEl = el; } update(dt) { const stats = this.game.player.stats; // Stat bars this.updateBar('health', stats.health); this.updateBar('hunger', stats.hunger); this.updateBar('warmth', stats.warmth); this.updateBar('mood', stats.mood); this.updateBar('hygiene', stats.hygiene); // Critical indicators this.setCritical('health', stats.health < 20); this.setCritical('hunger', stats.hunger < 15); this.setCritical('warmth', stats.warmth < 20); this.setCritical('mood', stats.mood < 10); this.setCritical('hygiene', stats.hygiene < 20); // Money document.getElementById('val-money').textContent = Math.floor(stats.money); // Time document.getElementById('val-time').textContent = this.game.getTimeString(); document.getElementById('val-day').textContent = `День ${this.game.gameDay}`; // Weather + season document.getElementById('val-weather').textContent = this.game.weather.getIcon(); document.getElementById('val-temp').textContent = `${Math.round(this.game.weather.temperature)}°C`; document.getElementById('val-season').textContent = `${this.game.seasons.getIcon()} ${this.game.seasons.getName()}`; // Protection & warmth bonuses const protEl = document.getElementById('val-protection'); const warmthBonusEl = document.getElementById('val-warmth-bonus'); if (protEl) protEl.textContent = this.game.equipment.getProtectionBonus(); if (warmthBonusEl) warmthBonusEl.textContent = this.game.equipment.getWarmthBonus(); // Stamina this.updateStaminaBar(); // Reputation this.updateReputationDisplay(); // Dog this.updateDogIndicator(); // Danger this.updateDangerWarning(); // Compass this.updateCompass(); // Equipment HUD this.updateEquipmentHUD(); // Quest Tracker this.updateQuestTracker(); // Minimap (скрыть внутри зданий) const minimapCanvas = document.getElementById('minimap'); if (minimapCanvas) { minimapCanvas.style.display = this.game.interiors?.isInside ? 'none' : 'block'; } if (!this.game.interiors?.isInside) { this.renderMinimap(); } } updateStaminaBar() { let bar = document.getElementById('stamina-bar'); if (!bar) { bar = document.createElement('div'); bar.id = 'stamina-bar'; bar.innerHTML = '
'; document.getElementById('hud').appendChild(bar); } const fill = bar.querySelector('.stamina-fill'); const pct = this.game.player.stamina / this.game.player.maxStamina * 100; fill.style.width = pct + '%'; bar.style.opacity = pct < 100 ? '1' : '0'; } updateDogIndicator() { if (!this.game.dog.adopted) return; let el = document.getElementById('dog-indicator'); if (!el) { el = document.createElement('div'); el.id = 'dog-indicator'; el.style.cssText = 'position:absolute;bottom:68px;right:15px;background:linear-gradient(135deg,rgba(10,10,20,0.85),rgba(20,20,35,0.75));padding:4px 12px;border-radius:8px;font-size:0.75rem;color:#c8a040;pointer-events:none;backdrop-filter:blur(8px);border:1px solid rgba(200,160,64,0.15);'; document.getElementById('hud').appendChild(el); } el.textContent = '🐕 Шарик рядом'; } updateReputationDisplay() { const el = document.getElementById('reputation-display'); if (!el) return; const rep = this.game.reputation; el.style.color = rep.getColor(); el.textContent = `⭐ ${rep.getLevel()} (${rep.value > 0 ? '+' : ''}${rep.value})`; } updateDangerWarning() { let el = document.getElementById('danger-warning'); if (this.game.dangers.hasNearbyDanger()) { if (!el) { el = document.createElement('div'); el.id = 'danger-warning'; el.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) translateY(-80px);color:#f44336;font-size:0.9rem;font-weight:700;pointer-events:none;text-shadow:0 0 15px rgba(244,67,54,0.5);animation:criticalPulse 1s infinite;padding:8px 20px;background:linear-gradient(135deg,rgba(60,0,0,0.8),rgba(40,0,0,0.7));border-radius:8px;border:1px solid rgba(244,67,54,0.3);backdrop-filter:blur(8px);'; document.getElementById('hud').appendChild(el); } el.textContent = '⚠ ОПАСНОСТЬ! [Space] — отбиться'; el.style.display = 'block'; } else if (el) { el.style.display = 'none'; } } updateEquipmentHUD() { let el = document.getElementById('equipment-hud'); if (!el) { el = document.createElement('div'); el.id = 'equipment-hud'; el.style.cssText = 'position:absolute;bottom:15px;right:15px;background:linear-gradient(135deg,rgba(10,10,20,0.85),rgba(20,20,35,0.75));padding:6px 10px;border-radius:8px;font-size:0.7rem;pointer-events:none;backdrop-filter:blur(8px);display:flex;gap:8px;border:1px solid rgba(255,255,255,0.05);'; document.getElementById('hud').appendChild(el); } const eq = this.game.equipment; const slotIcons = { head: '🧢', body: '🧥', feet: '🥾', hands: '🧤' }; let html = ''; for (const [slot, defaultIcon] of Object.entries(slotIcons)) { const item = eq.getEquipped(slot); const icon = item ? item.icon : defaultIcon; const opacity = item ? '1' : '0.25'; html += `${icon}`; } el.innerHTML = html; } updateQuestTracker() { const tracker = document.getElementById('quest-tracker'); if (!tracker) return; const active = this.game.questSystem.getActiveQuests(); if (active.length === 0) { tracker.style.display = 'none'; return; } tracker.style.display = 'block'; const quest = active[0]; // Show first active quest const pct = Math.min(100, (quest.progress / quest.target) * 100); document.getElementById('tracker-title').textContent = quest.title; document.getElementById('tracker-progress').textContent = `${Math.min(quest.progress, quest.target)} / ${quest.target}`; document.getElementById('tracker-fill').style.width = `${pct}%`; } // === Job Progress === updateJobProgress(progress, name) { let el = document.getElementById('job-progress'); if (!el) { el = document.createElement('div'); el.id = 'job-progress'; el.innerHTML = `
[H] — отмена
`; el.style.cssText = 'position:fixed;bottom:140px;left:50%;transform:translateX(-50%);z-index:12;width:220px;pointer-events:none;text-align:center;'; el.querySelector('.job-label').style.cssText = 'font-size:0.8rem;color:#8fc;margin-bottom:4px;font-weight:600;'; el.querySelector('.job-bar-bg').style.cssText = 'height:5px;background:rgba(255,255,255,0.06);border-radius:3px;overflow:hidden;'; el.querySelector('.job-bar-fill').style.cssText = 'height:100%;background:linear-gradient(90deg,#43a047,#66bb6a);border-radius:3px;transition:width 0.15s;'; el.querySelector('.job-cancel').style.cssText = 'font-size:0.6rem;color:#666;margin-top:4px;'; document.body.appendChild(el); } el.style.display = 'block'; el.querySelector('.job-label').textContent = `🔧 ${name}`; el.querySelector('.job-bar-fill').style.width = `${Math.min(100, progress * 100)}%`; } hideJobProgress() { const el = document.getElementById('job-progress'); if (el) el.style.display = 'none'; } updateBar(name, value) { const bar = document.getElementById(`bar-${name}`); const val = document.getElementById(`val-${name}`); if (bar) bar.style.width = `${Math.max(0, Math.min(100, value))}%`; if (val) val.textContent = Math.floor(value); } setCritical(name, isCritical) { const bars = document.querySelectorAll('.stat-bar'); const names = ['health', 'hunger', 'warmth', 'mood']; const idx = names.indexOf(name); if (idx >= 0 && bars[idx]) { if (isCritical) { bars[idx].classList.add('critical'); } else { bars[idx].classList.remove('critical'); } } } updateCompass() { const dir = document.getElementById('compass-dir'); if (!dir) return; const yaw = this.game.cameraController.yaw; const deg = ((yaw * 180 / Math.PI) % 360 + 360) % 360; let label; if (deg > 315 || deg <= 45) label = 'С'; else if (deg > 45 && deg <= 135) label = 'З'; else if (deg > 135 && deg <= 225) label = 'Ю'; else label = 'В'; dir.textContent = label; } updateInteractionHint(interactable) { const hint = document.getElementById('interaction-hint'); const text = document.getElementById('hint-text'); if (interactable) { hint.classList.remove('hidden'); text.textContent = interactable.label; } else { hint.classList.add('hidden'); } } // === Tooltip === showTooltip(x, y, title, desc, statsHtml) { const tt = this.tooltipEl; if (!tt) return; document.getElementById('tooltip-title').textContent = title; document.getElementById('tooltip-desc').textContent = desc; document.getElementById('tooltip-stats').innerHTML = statsHtml || ''; tt.classList.remove('hidden'); tt.style.left = Math.min(x + 12, window.innerWidth - 260) + 'px'; tt.style.top = Math.min(y - 10, window.innerHeight - 150) + 'px'; } hideTooltip() { if (this.tooltipEl) this.tooltipEl.classList.add('hidden'); } // === Minimap === renderMinimap() { const ctx = this.minimapCtx; if (!ctx) return; const w = 180, h = 180; const scale = 0.9; // Уменьшили масштаб чтобы больше карты видно const player = this.game.player; ctx.clearRect(0, 0, w, h); // Background const bgGrad = ctx.createRadialGradient(w/2, h/2, 0, w/2, h/2, w/2); bgGrad.addColorStop(0, '#1a1a24'); bgGrad.addColorStop(1, '#111118'); ctx.fillStyle = bgGrad; ctx.beginPath(); ctx.arc(w / 2, h / 2, w / 2, 0, Math.PI * 2); ctx.fill(); ctx.save(); ctx.beginPath(); ctx.arc(w / 2, h / 2, w / 2 - 1, 0, Math.PI * 2); ctx.clip(); const cx = w / 2; const cy = h / 2; const px = player.position.x; const pz = player.position.z; // Дороги из конфига const roads = this.game.world.mapConfig?.roads || []; ctx.fillStyle = '#2a2a34'; roads.forEach(road => { const rx = road.x ?? 0; const rz = road.z ?? 0; const rw = road.width ?? 100; const rh = road.height ?? 8; const isRotated = (road.rotation || 0) > 0.5; // NS road if (isRotated) { // NS road: width becomes vertical extent, height becomes horizontal width const roadX = cx + (rx - px) * scale - (rh / 2) * scale; const roadY = cy + (rz - pz) * scale - (rw / 2) * scale; ctx.fillRect(roadX, roadY, rh * scale, rw * scale); } else { // EW road: normal const roadX = cx + (rx - px) * scale - (rw / 2) * scale; const roadY = cy + (rz - pz) * scale - (rh / 2) * scale; ctx.fillRect(roadX, roadY, rw * scale, rh * scale); } }); // Здания ctx.fillStyle = '#3a3a48'; this.game.world.buildingRects.forEach(b => { const bx = cx + (b.x - px) * scale - (b.w / 2) * scale; const by = cy + (b.z - pz) * scale - (b.d / 2) * scale; ctx.fillRect(bx, by, b.w * scale, b.d * scale); }); // Парк const parkCfg = this.game.world.mapConfig?.structures?.park || {}; ctx.fillStyle = '#1a4a1a'; ctx.beginPath(); ctx.arc(cx + ((parkCfg.x ?? -30) - px) * scale, cy + ((parkCfg.z ?? 25) - pz) * scale, (parkCfg.radius ?? 18) * scale, 0, Math.PI * 2); ctx.fill(); // Укрытие const shelCfg = this.game.world.mapConfig?.structures?.shelter || {}; ctx.fillStyle = '#5a4a3a'; const shelX = cx + ((shelCfg.x ?? -35) - px) * scale - 4 * scale; const shelZ = cy + ((shelCfg.z ?? 35) - pz) * scale - 3 * scale; this.roundRect(ctx, shelX, shelZ, 8 * scale, 6 * scale, 2); // Больница (белый квадрат с красной точкой, БЕЗ креста) const hospCfg = this.game.world.mapConfig?.structures?.hospital || {}; ctx.fillStyle = '#dddddd'; const hospX = cx + ((hospCfg.x ?? -45) - px) * scale - 6 * scale; const hospZ = cy + ((hospCfg.z ?? -55) - pz) * scale - 5 * scale; this.roundRect(ctx, hospX, hospZ, 12 * scale, 10 * scale, 2); ctx.fillStyle = '#ee3333'; ctx.beginPath(); ctx.arc(cx + ((hospCfg.x ?? -45) - px) * scale, cy + ((hospCfg.z ?? -55) - pz) * scale, 3, 0, Math.PI * 2); ctx.fill(); // Стройка (контур) const conCfg = this.game.world.mapConfig?.structures?.construction || {}; ctx.strokeStyle = '#aa7020'; ctx.lineWidth = 1.5; const csX = cx + ((conCfg.x ?? 70) - px) * scale - 4.5 * scale; const csZ = cy + ((conCfg.z ?? 60) - pz) * scale - 4.5 * scale; ctx.strokeRect(csX, csZ, 9 * scale, 9 * scale); // Рынок const mktCfg = this.game.world.mapConfig?.structures?.market || {}; ctx.fillStyle = '#cc7020'; const mkX = cx + ((mktCfg.x ?? 35) - px) * scale - 7 * scale; const mkZ = cy + ((mktCfg.z ?? -55) - pz) * scale - 5 * scale; this.roundRect(ctx, mkX, mkZ, 14 * scale, 10 * scale, 2); // Лагерь игрока if (this.game.housing.built) { ctx.fillStyle = '#33aa33'; const campX = cx + (this.game.housing.position.x - px) * scale; const campZ = cy + (this.game.housing.position.z - pz) * scale; ctx.beginPath(); ctx.arc(campX, campZ, 4, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#55cc55'; ctx.lineWidth = 1; ctx.stroke(); } // NPC this.game.npcManager.npcs.forEach(npc => { const nx = cx + (npc.position.x - px) * scale; const ny = cy + (npc.position.z - pz) * scale; ctx.fillStyle = '#4488ee'; ctx.beginPath(); ctx.arc(nx, ny, 3, 0, Math.PI * 2); ctx.fill(); }); // Мусорки ctx.fillStyle = '#4a7a4a'; this.game.world.interactables.forEach(obj => { if (obj.type === 'dumpster' || obj.type === 'trashpile') { const dx = cx + (obj.position.x - px) * scale; const dy = cy + (obj.position.z - pz) * scale; ctx.fillRect(dx - 1.5, dy - 1.5, 3, 3); } }); // Пёс if (this.game.dog.adopted && this.game.dog.mesh) { ctx.fillStyle = '#c8a040'; const dogX = cx + (this.game.dog.position.x - px) * scale; const dogY = cy + (this.game.dog.position.z - pz) * scale; ctx.beginPath(); ctx.arc(dogX, dogY, 2.5, 0, Math.PI * 2); ctx.fill(); } // Враги this.game.dangers.enemies.forEach(enemy => { const ex = cx + (enemy.position.x - px) * scale; const ey = cy + (enemy.position.z - pz) * scale; ctx.fillStyle = '#f44336'; ctx.beginPath(); ctx.arc(ex, ey, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = 'rgba(244,67,54,0.4)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(ex, ey, 5, 0, Math.PI * 2); ctx.stroke(); }); // Полиция this.game.police.officers.forEach(officer => { if (!officer.mesh || !officer.mesh.visible) return; const ox = cx + (officer.position.x - px) * scale; const oy = cy + (officer.position.z - pz) * scale; ctx.fillStyle = '#4488ff'; ctx.beginPath(); ctx.arc(ox, oy, 3, 0, Math.PI * 2); ctx.fill(); if (officer.state === 'chase') { ctx.strokeStyle = 'rgba(68,136,255,0.5)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(ox, oy, 5, 0, Math.PI * 2); ctx.stroke(); } }); // Доска объявлений const jbCfg = this.game.world.mapConfig?.structures?.jobBoard || {}; ctx.fillStyle = '#4caf50'; const jbX = cx + ((jbCfg.x ?? 20) - px) * scale; const jbZ = cy + ((jbCfg.z ?? -8) - pz) * scale; ctx.fillRect(jbX - 2.5, jbZ - 2.5, 5, 5); // Маркер активного квеста const activeQuests = this.game.questSystem.quests.filter(q => !q.completed && q.location); if (activeQuests.length > 0) { const quest = activeQuests[0]; const qx = cx + (quest.location.x - px) * scale; const qy = cy + (quest.location.z - pz) * scale; const pulse = 0.8 + Math.sin(Date.now() / 300) * 0.3; const qs = 5 * pulse; ctx.fillStyle = '#ffdd00'; ctx.beginPath(); ctx.moveTo(qx, qy - qs); ctx.lineTo(qx + qs * 0.6, qy); ctx.lineTo(qx, qy + qs); ctx.lineTo(qx - qs * 0.6, qy); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#aa8800'; ctx.lineWidth = 1; ctx.stroke(); } // Игрок (стрелка) ctx.save(); ctx.translate(cx, cy); const yaw = this.game.cameraController.yaw; ctx.rotate(-yaw); ctx.fillStyle = 'rgba(240,160,64,0.15)'; ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#f0a040'; ctx.beginPath(); ctx.moveTo(0, -7); ctx.lineTo(-4.5, 5); ctx.lineTo(0, 3); ctx.lineTo(4.5, 5); ctx.closePath(); ctx.fill(); ctx.restore(); ctx.restore(); // Рамка ctx.strokeStyle = 'rgba(240,160,64,0.3)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(w / 2, h / 2, w / 2 - 1, 0, Math.PI * 2); ctx.stroke(); // Стороны света ctx.fillStyle = 'rgba(240,160,64,0.5)'; ctx.font = '8px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('С', w/2, 12); ctx.fillText('Ю', w/2, h - 5); ctx.fillText('З', 8, h/2 + 3); ctx.fillText('В', w - 8, h/2 + 3); } roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); ctx.fill(); } // === Beg progress === showBegProgress(show) { if (this.begProgressEl) { if (show) { this.begProgressEl.classList.remove('hidden'); } else { this.begProgressEl.classList.add('hidden'); } } } updateBegProgress(ratio) { if (!this.begProgressEl) return; const fill = this.begProgressEl.querySelector('.beg-bar-fill'); if (fill) fill.style.width = `${Math.min(100, ratio * 100)}%`; } // === Busk progress === showBuskProgress(show) { if (this.buskProgressEl) { if (show) { this.buskProgressEl.classList.remove('hidden'); } else { this.buskProgressEl.classList.add('hidden'); } } } updateBuskProgress(ratio) { if (!this.buskProgressEl) return; const fill = this.buskProgressEl.querySelector('.beg-bar-fill'); if (fill) fill.style.width = `${Math.min(100, ratio * 100)}%`; } // === Dialogs === showDialog(speaker, text, choices, callback) { const box = document.getElementById('dialog-box'); document.getElementById('dialog-speaker').textContent = speaker; document.getElementById('dialog-text').textContent = text; const choicesEl = document.getElementById('dialog-choices'); choicesEl.innerHTML = ''; this.game.sound.playDialogOpen(); choices.forEach((choice, i) => { const btn = document.createElement('button'); btn.className = 'dialog-choice'; btn.textContent = choice; btn.addEventListener('click', () => callback(i)); choicesEl.appendChild(btn); }); box.classList.remove('hidden'); this.dialogCallback = callback; document.exitPointerLock(); } hideDialog() { document.getElementById('dialog-box').classList.add('hidden'); this.dialogCallback = null; } // === Inventory === toggleInventory() { this.inventoryOpen = !this.inventoryOpen; const screen = document.getElementById('inventory-screen'); if (this.inventoryOpen) { this.renderInventory(); screen.classList.remove('hidden'); document.exitPointerLock(); } else { screen.classList.add('hidden'); this.hideTooltip(); } } renderInventory() { const container = document.querySelector('.inventory-content'); // Equipment section this.renderEquipmentSection(container); const grid = document.getElementById('inventory-grid'); grid.innerHTML = ''; const items = this.game.inventory.getAll(); items.forEach(item => { const slot = document.createElement('div'); slot.className = 'inv-slot'; const isEquippable = item.equippable; const isUsable = item.usable; if (isEquippable) slot.classList.add('equippable'); slot.innerHTML = ` ${item.icon} ${item.name} ${item.count > 1 ? `x${item.count}` : ''} `; // Tooltip on hover slot.addEventListener('mouseenter', (e) => { let statsHtml = ''; if (item.equippable) { const eqData = this.game.equipment.allItems[item.key.replace('eq_', '')]; if (eqData) { statsHtml = `🔥 +${eqData.warmth} Тепло
🛡️ +${eqData.protection} Защита
😊 +${eqData.mood} Настроение`; } } if (item.usable && !item.equippable) { statsHtml = 'Нажмите чтобы использовать'; } this.showTooltip(e.clientX, e.clientY, item.name, item.desc || '', statsHtml); }); slot.addEventListener('mouseleave', () => this.hideTooltip()); slot.addEventListener('mousemove', (e) => { if (this.tooltipEl && !this.tooltipEl.classList.contains('hidden')) { this.tooltipEl.style.left = Math.min(e.clientX + 12, window.innerWidth - 260) + 'px'; this.tooltipEl.style.top = Math.min(e.clientY - 10, window.innerHeight - 150) + 'px'; } }); if (isEquippable || isUsable) { slot.style.cursor = 'pointer'; slot.addEventListener('click', () => { this.game.inventory.useItem(item.key); this.renderInventory(); }); } grid.appendChild(slot); }); const empty = this.game.inventory.maxSlots - items.length; for (let i = 0; i < Math.max(0, empty); i++) { const slot = document.createElement('div'); slot.className = 'inv-slot'; slot.style.opacity = '0.3'; grid.appendChild(slot); } // Crafting section this.renderCrafting(container); } renderEquipmentSection(container) { let eqSection = container.querySelector('.equipment-section'); if (!eqSection) { eqSection = document.createElement('div'); eqSection.className = 'equipment-section'; container.insertBefore(eqSection, document.getElementById('inventory-grid')); } const eq = this.game.equipment; const slotNames = { head: { label: 'Голова', icon: '🧢' }, body: { label: 'Тело', icon: '🧥' }, feet: { label: 'Ноги', icon: '🥾' }, hands: { label: 'Руки', icon: '🧤' } }; let html = '
Экипировка
'; for (const [slot, info] of Object.entries(slotNames)) { const item = eq.getEquipped(slot); const isEmpty = !item; html += `
${item ? item.icon : info.icon} ${item ? item.name : info.label} ${item ? `+${item.warmth}🔥 +${item.protection}🛡️` : ''}
`; } html += '
'; // Total bonuses const warmth = eq.getWarmthBonus(); const protection = eq.getProtectionBonus(); const mood = eq.getMoodBonus(); if (warmth > 0 || protection > 0 || mood > 0) { html += `
`; if (warmth > 0) html += `+${warmth} 🔥 `; if (protection > 0) html += `+${protection}% 🛡️ `; if (mood > 0) html += `+${mood} 😊`; html += '
'; } eqSection.innerHTML = html; // Unequip handlers eqSection.querySelectorAll('.eq-slot:not(.empty)').forEach(slotEl => { slotEl.addEventListener('click', () => { const slotKey = slotEl.dataset.slot; eq.unequip(slotKey); this.renderInventory(); }); }); } renderCrafting(container) { let craftSection = container.querySelector('.craft-section'); if (craftSection) craftSection.remove(); craftSection = document.createElement('div'); craftSection.className = 'craft-section'; craftSection.innerHTML = '

Крафт

'; const recipes = this.game.inventory.recipes; const inv = this.game.inventory; recipes.forEach(recipe => { const canCraft = inv.canCraft(recipe); const el = document.createElement('div'); el.className = `craft-item ${canCraft ? 'available' : ''}`; // Build ingredients line with "has/needs" info let ingredientsHtml = ''; if (recipe.ingredients) { const parts = []; for (const [key, need] of Object.entries(recipe.ingredients)) { const have = inv.getCount(key); const itemData = inv.itemData[key]; const name = itemData ? itemData.name : key; const cls = have >= need ? 'has' : 'missing'; parts.push(`${name}: ${have}/${need}`); } ingredientsHtml = `
${parts.join('   ')}
`; } el.innerHTML = `
${inv.itemData[recipe.result]?.icon || ''} ${recipe.name}
${recipe.desc}
${ingredientsHtml}
`; if (canCraft) { el.querySelector('.craft-btn').addEventListener('click', () => { inv.craft(recipe); this.renderInventory(); }); } craftSection.appendChild(el); }); const closeBtn = container.querySelector('.panel-close'); container.appendChild(craftSection); } // === Quests === toggleQuests() { this.questsOpen = !this.questsOpen; const screen = document.getElementById('quest-screen'); if (this.questsOpen) { this.renderQuests(); screen.classList.remove('hidden'); document.exitPointerLock(); } else { screen.classList.add('hidden'); } } renderQuests() { const list = document.getElementById('quest-list'); list.innerHTML = ''; const active = this.game.questSystem.getActiveQuests(); const completed = this.game.questSystem.getCompletedQuests(); if (active.length === 0 && completed.length === 0) { list.innerHTML = '

Нет активных квестов

'; return; } active.forEach(quest => { const pct = Math.min(100, (quest.progress / quest.target) * 100); const el = document.createElement('div'); el.className = 'quest-item active'; el.innerHTML = `
📌 ${quest.title}
${quest.description}
${Math.min(quest.progress, quest.target)} / ${quest.target}
`; list.appendChild(el); }); if (completed.length > 0) { const divider = document.createElement('div'); divider.style.cssText = 'font-size:0.7rem;color:#444;text-transform:uppercase;letter-spacing:0.1em;margin:16px 0 8px;padding-bottom:6px;border-bottom:1px solid rgba(255,255,255,0.04);'; divider.textContent = 'Выполнено'; list.appendChild(divider); } completed.forEach(quest => { const el = document.createElement('div'); el.className = 'quest-item completed'; el.innerHTML = `
✅ ${quest.title}
${quest.description}
`; list.appendChild(el); }); } // === Skills === toggleSkills() { this.skillsOpen = !this.skillsOpen; const screen = document.getElementById('skills-screen'); if (this.skillsOpen) { this.renderSkills(); screen.classList.remove('hidden'); document.exitPointerLock(); } else { screen.classList.add('hidden'); } } renderSkills() { const list = document.getElementById('skills-list'); list.innerHTML = ''; const icons = { scavenging: '🔍', begging: '🗣️', survival: '🏕️', trading: '💰' }; const descs = { scavenging: 'Больше находок при обыске', begging: 'Больше денег от милостыни', survival: 'Медленнее теряете тепло и сытость', trading: 'Лучшие цены при работе' }; const skills = this.game.skills.skills; for (const [key, skill] of Object.entries(skills)) { const xpPct = Math.min(100, (skill.xp / skill.xpNeeded) * 100); const maxed = skill.level >= this.game.skills.maxLevel; const el = document.createElement('div'); el.className = 'skill-item'; el.innerHTML = `
${icons[key] || ''} ${skill.name} ${maxed ? '✨ MAX' : `Ур. ${skill.level}`}
${skill.desc || descs[key] || ''}
${!maxed ? `
${skill.xp} / ${skill.xpNeeded} XP
` : ''} `; list.appendChild(el); } } // === Achievements === toggleAchievements() { this.achievementsOpen = !this.achievementsOpen; const screen = document.getElementById('achievements-screen'); if (this.achievementsOpen) { this.renderAchievements(); screen.classList.remove('hidden'); document.exitPointerLock(); } else { screen.classList.add('hidden'); } } renderAchievements() { const list = document.getElementById('achievements-list'); list.innerHTML = ''; const achievements = this.game.achievements; const progress = achievements.getProgress(); // Progress header const headerEl = document.createElement('div'); headerEl.className = 'ach-progress-header'; headerEl.innerHTML = `
Разблокировано: ${progress.unlocked} / ${progress.total}
`; list.appendChild(headerEl); const categories = { survival: { name: 'Выживание', icon: '🏕️' }, social: { name: 'Социальные', icon: '🤝' }, economy: { name: 'Экономика', icon: '💰' }, combat: { name: 'Боевые', icon: '⚔️' }, explore: { name: 'Исследование', icon: '🗺️' } }; for (const [catKey, catInfo] of Object.entries(categories)) { const catAchievements = achievements.getByCategory(catKey); if (catAchievements.length === 0) continue; const catEl = document.createElement('div'); catEl.className = 'ach-category'; const unlockedCount = catAchievements.filter(a => achievements.unlocked.has(a.id)).length; catEl.innerHTML = `
${catInfo.icon} ${catInfo.name} ${unlockedCount}/${catAchievements.length}
`; catAchievements.forEach(ach => { const unlocked = achievements.unlocked.has(ach.id); const achEl = document.createElement('div'); achEl.className = `ach-item ${unlocked ? 'unlocked' : 'locked'}`; achEl.innerHTML = ` ${unlocked ? ach.icon : '🔒'}
${unlocked ? ach.title : '???'}
${ach.desc}
`; catEl.appendChild(achEl); }); list.appendChild(catEl); } } // === Intro === showIntro(callback) { const overlay = document.getElementById('intro-overlay'); const textEl = document.getElementById('intro-text'); overlay.classList.remove('hidden'); const lines = [ 'Ещё вчера у вас было всё — работа, квартира, друзья...', 'Одна ошибка, и жизнь перевернулась.', 'Теперь ваш дом — улица, а главная цель — дожить до завтра.', 'Ищите еду, собирайте бутылки, заводите знакомства.', 'Не сдавайтесь. Каждый день — это шанс.' ]; textEl.innerHTML = ''; lines.forEach((line, i) => { const p = document.createElement('p'); p.className = 'intro-line'; p.textContent = line; p.style.animationDelay = `${i * 1.2}s`; textEl.appendChild(p); }); const skipBtn = document.getElementById('btn-skip-intro'); const closeIntro = () => { overlay.classList.add('hidden'); skipBtn.removeEventListener('click', closeIntro); if (callback) callback(); }; skipBtn.addEventListener('click', closeIntro); setTimeout(closeIntro, lines.length * 1200 + 2000); } // === Notifications === showNotification(text, type) { const container = document.getElementById('notifications'); const notif = document.createElement('div'); notif.className = 'notification'; if (type === 'good') notif.classList.add('good'); if (type === 'bad') notif.classList.add('bad'); notif.textContent = text; container.appendChild(notif); setTimeout(() => notif.remove(), 3600); } // === Death === showDeathScreen(reason, day) { const screen = document.getElementById('death-screen'); document.getElementById('death-reason').textContent = reason; const money = Math.floor(this.game.player.stats.money); const completedQuests = this.game.questSystem.getCompletedQuests().length; const totalQuests = this.game.questSystem.quests.length; const hasDog = this.game.dog.adopted; const skills = this.game.skills.skills; const totalLevels = Object.values(skills).reduce((s, sk) => s + sk.level, 0); const repLevel = this.game.reputation.getLevel(); const achProgress = this.game.achievements.getProgress(); const eqSlots = this.game.equipment.getFilledSlots(); document.getElementById('death-stats').innerHTML = `
📅 ${day} Дней
💰 ${money}₽ Денег
📋 ${completedQuests}/${totalQuests} Квесты
${totalLevels} Навыки
${repLevel} Репутация
🏆 ${achProgress.unlocked}/${achProgress.total} Достижения
🛡️ ${eqSlots}/4 Экипировка
🏠 ${this.game.housing.built ? 'Да' : 'Нет'} Укрытие
🐕 ${hasDog ? 'Да' : 'Нет'} Пёс
`; screen.classList.remove('hidden'); document.getElementById('btn-restart').onclick = () => { screen.classList.add('hidden'); this.game.restart(); }; } hideDeathScreen() { document.getElementById('death-screen').classList.add('hidden'); } }