import * as THREE from 'three'; export class Player { constructor(game) { this.game = game; // Характеристики this.stats = { health: 100, hunger: 100, warmth: 100, mood: 50, money: 0, hygiene: 100 }; // Физика this.position = new THREE.Vector3(0, 0, 15); this.velocity = new THREE.Vector3(); this.direction = new THREE.Vector3(); this.speed = 5; this.sprintMultiplier = 1.7; this.height = 1.7; this.radius = 0.4; // Mesh this.mesh = null; this.nearestInteractable = null; // Таймеры this.statTimer = 0; this.isSleeping = false; this.sleepTimer = 0; this.isWarming = false; this._shelterSleep = false; // Begging this.isBegging = false; this.begTimer = 0; this.begCooldown = 0; this.begDuration = 3; // Busking this.isBusking = false; this.buskTimer = 0; this.buskCooldown = 0; this.buskDuration = 5; // Hygiene & Disease this.isDiseased = false; this.diseaseTimer = 0; this.diseaseCheckTimer = 0; this.washCooldown = 0; // Addiction this.addictionLevel = 0; this.withdrawalTimer = 0; this.lastDrinkTime = 0; // Шаги this.stepTimer = 0; this.stepInterval = 0.45; // Предыдущее здоровье this._prevHealth = 100; // Head bob this.bobPhase = 0; this.bobAmount = 0; // Stamina this.stamina = 100; this.maxStamina = 100; this.isSprinting = false; // Сытость 100 таймер (для ачивки) this._wellFedTimer = 0; } spawn() { this.createMesh(); this.position.set(0, 0, 15); this.mesh.position.copy(this.position); this.game.camera.position.set(0, this.height, 15); } createMesh() { const group = new THREE.Group(); const bodyGeo = new THREE.CylinderGeometry(0.3, 0.35, 1.0, 8); const bodyMat = new THREE.MeshStandardMaterial({ color: 0x5c4033 }); const body = new THREE.Mesh(bodyGeo, bodyMat); body.position.y = 0.8; body.castShadow = true; group.add(body); const headGeo = new THREE.SphereGeometry(0.22, 8, 6); const headMat = new THREE.MeshStandardMaterial({ color: 0xd4a574 }); const head = new THREE.Mesh(headGeo, headMat); head.position.y = 1.5; head.castShadow = true; group.add(head); const hatGeo = new THREE.CylinderGeometry(0.25, 0.28, 0.2, 8); const hatMat = new THREE.MeshStandardMaterial({ color: 0x333355 }); const hat = new THREE.Mesh(hatGeo, hatMat); hat.position.y = 1.7; group.add(hat); const armMat = new THREE.MeshStandardMaterial({ color: 0x5c4033 }); [-0.4, 0.4].forEach(side => { const armGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.7, 6); const arm = new THREE.Mesh(armGeo, armMat); arm.position.set(side, 0.7, 0); arm.rotation.z = side > 0 ? -0.15 : 0.15; arm.castShadow = true; group.add(arm); }); const legMat = new THREE.MeshStandardMaterial({ color: 0x333344 }); [-0.15, 0.15].forEach(side => { const legGeo = new THREE.CylinderGeometry(0.1, 0.1, 0.6, 6); const leg = new THREE.Mesh(legGeo, legMat); leg.position.set(side, 0.3, 0); leg.castShadow = true; group.add(leg); }); this.mesh = group; group.traverse(child => { if (child.isMesh) { child.castShadow = true; child.layers.set(1); } }); this.game.scene.add(group); } reset() { if (this.mesh) { this.game.scene.remove(this.mesh); } this.stats = { health: 100, hunger: 100, warmth: 100, mood: 50, money: 0, hygiene: 100 }; this.stamina = 100; this.isSleeping = false; this.isWarming = false; this.isBegging = false; this.begTimer = 0; this.begCooldown = 0; this.isBusking = false; this.buskTimer = 0; this.buskCooldown = 0; this.isDiseased = false; this.diseaseTimer = 0; this.diseaseCheckTimer = 0; this.washCooldown = 0; this.addictionLevel = 0; this.withdrawalTimer = 0; this.lastDrinkTime = 0; this.nearestInteractable = null; this._prevHealth = 100; this._shelterSleep = false; this._wellFedTimer = 0; } update(dt) { if (this.isSleeping) { this.updateSleep(dt); this.updateScreenEffects(); return; } this.updateMovement(dt); this.updateStats(dt); this.updateBegging(dt); this.updateBusking(dt); this.checkInteractables(); this.updateScreenEffects(); this.checkDeath(); this.trackLocations(); if (this.begCooldown > 0) this.begCooldown -= dt; if (this.buskCooldown > 0) this.buskCooldown -= dt; } updateMovement(dt) { const keys = this.game.keys; const camera = this.game.camera; if (this.isBegging || this.isBusking) return; const forward = new THREE.Vector3(); camera.getWorldDirection(forward); forward.y = 0; forward.normalize(); const right = new THREE.Vector3(); right.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize(); this.direction.set(0, 0, 0); if (keys['KeyW']) this.direction.add(forward); if (keys['KeyS']) this.direction.sub(forward); if (keys['KeyD']) this.direction.add(right); if (keys['KeyA']) this.direction.sub(right); if (this.direction.length() > 0) { this.direction.normalize(); const wantSprint = keys['ShiftLeft'] || keys['ShiftRight']; const sprint = wantSprint && this.stamina > 0; this.isSprinting = sprint; if (sprint) { this.stamina = Math.max(0, this.stamina - 20 * dt); } else { this.stamina = Math.min(this.maxStamina, this.stamina + 10 * dt); } const moveSpeed = this.speed * (sprint ? this.sprintMultiplier : 1); let speedMod = 1; if (this.stats.hunger < 20) speedMod *= 0.7; if (this.stats.warmth < 15) speedMod *= 0.8; if (this.stats.health < 25) speedMod *= 0.6; if (this.isDiseased) speedMod *= 0.8; const newPos = this.position.clone().add( this.direction.clone().multiplyScalar(moveSpeed * speedMod * dt) ); if (!this.checkCollision(newPos)) { this.position.copy(newPos); } if (this.game.interiors && this.game.interiors.isInside) { this.position.x = THREE.MathUtils.clamp(this.position.x, 490, 510); this.position.z = THREE.MathUtils.clamp(this.position.z, -10, 120); } else { this.position.x = THREE.MathUtils.clamp(this.position.x, -95, 95); this.position.z = THREE.MathUtils.clamp(this.position.z, -95, 95); } this.stepTimer += dt * (sprint ? 1.5 : 1); if (this.stepTimer >= this.stepInterval) { this.stepTimer = 0; this.game.sound.playStep(); } this.bobPhase += dt * (sprint ? 14 : 10); this.bobAmount = THREE.MathUtils.lerp(this.bobAmount, 0.04, dt * 5); } else { this.bobAmount = THREE.MathUtils.lerp(this.bobAmount, 0, dt * 8); this.isSprinting = false; this.stamina = Math.min(this.maxStamina, this.stamina + 15 * dt); } this.mesh.position.copy(this.position); const bobY = Math.sin(this.bobPhase) * this.bobAmount; // Withdrawal tremor let tremorX = 0, tremorY = 0; if (this.addictionLevel > 40 && this.withdrawalTimer > 0) { const intensity = Math.min(0.02, this.addictionLevel * 0.0003); tremorX = (Math.random() - 0.5) * intensity; tremorY = (Math.random() - 0.5) * intensity; } this.game.camera.position.set( this.position.x + tremorX, this.position.y + this.height + bobY + tremorY, this.position.z ); const camDir = new THREE.Vector3(); camera.getWorldDirection(camDir); this.mesh.rotation.y = Math.atan2(camDir.x, camDir.z); } checkCollision(newPos) { const playerBox = new THREE.Box3( new THREE.Vector3(newPos.x - this.radius, 0, newPos.z - this.radius), new THREE.Vector3(newPos.x + this.radius, this.height, newPos.z + this.radius) ); // Статические коллайдеры (здания, мебель и т.д.) for (const collider of this.game.world.colliders) { if (playerBox.intersectsBox(collider)) return true; } // Динамические коллайдеры движущихся машин for (const collider of this.game.world.vehicleColliders) { if (!collider.isEmpty() && playerBox.intersectsBox(collider)) return true; } return false; } updateStats(dt) { this.statTimer += dt; if (this.statTimer < 1) return; this.statTimer = 0; this._prevHealth = this.stats.health; // Голод const survMod = this.game.skills.getSurvivalModifier(); const hungerDrain = this.game.seasons.getHungerDrain(); const diseaseMult = this.isDiseased ? 1.5 : 1; this.stats.hunger = Math.max(0, this.stats.hunger - 0.15 * survMod * hungerDrain * diseaseMult); // Тепло (с бонусом экипировки) const warmthDrain = this.game.seasons.getWarmthDrain(); const eqWarmth = this.game.equipment.getWarmthBonus(); const warmthReduction = Math.min(0.8, eqWarmth * 0.01); // Макс 80% снижение потери if (this.game.isNight()) { const drain = this.isWarming ? 0 : 0.3 * warmthDrain * (1 - warmthReduction); this.stats.warmth = Math.max(0, this.stats.warmth - drain); // В своём укрытии с крышей — меньше потеря if (this.game.housing.isPlayerInShelter() && this.game.housing.hasRoof()) { this.stats.warmth = Math.min(100, this.stats.warmth + 0.1); } } else { const warmthRegen = this.game.seasons.current === 'summer' ? 0.1 : 0.05; this.stats.warmth = Math.min(100, this.stats.warmth + warmthRegen); } if (this.isWarming) { this.stats.warmth = Math.min(100, this.stats.warmth + 0.5); } // Настроение от экипировки const eqMood = this.game.equipment.getMoodBonus(); if (eqMood > 0) { this.stats.mood = Math.min(100, this.stats.mood + eqMood * 0.02); } // Здоровье if (this.stats.hunger <= 0) { this.stats.health = Math.max(0, this.stats.health - 0.5); } if (this.stats.warmth <= 10) { this.stats.health = Math.max(0, this.stats.health - 0.3); } if (this.stats.hunger > 50 && this.stats.warmth > 50) { this.stats.health = Math.min(100, this.stats.health + 0.1); } // Гигиена let hygieneDrain = 0.03; if (this.game.weather.current === 'rain') hygieneDrain += 0.02; this.stats.hygiene = Math.max(0, this.stats.hygiene - hygieneDrain); // Болезнь if (this.isDiseased) { this.stats.health = Math.max(0, this.stats.health - 0.1); this.diseaseTimer--; if (this.diseaseTimer <= 0) { this.isDiseased = false; this.game.notify('Вы выздоровели!', 'good'); } } else if (this.stats.hygiene < 20) { this.diseaseCheckTimer++; if (this.diseaseCheckTimer >= 60) { this.diseaseCheckTimer = 0; if (Math.random() < 0.3) { this.isDiseased = true; this.diseaseTimer = 180; this.game.notify('Вы заболели! Нужна аптечка или больница.', 'bad'); } } } if (this.washCooldown > 0) this.washCooldown--; // Зависимость if (this.addictionLevel > 0) { this.addictionLevel = Math.max(0, this.addictionLevel - 0.008); } if (this.addictionLevel > 40) { const currentTime = this.game.gameTime + (this.game.gameDay - 1) * 24 * 60; const timeSinceDrink = currentTime - this.lastDrinkTime; if (timeSinceDrink > 30) { // Ломка this.withdrawalTimer++; if (this.withdrawalTimer % 60 === 0) { this.stats.mood = Math.max(0, this.stats.mood - 5); this.game.notify('Хочется выпить...', 'bad'); } } } // Квест: снизить зависимость до 0 if (this.addictionLevel <= 0 && this._hadAddiction) { this.game.questSystem.onEvent('sobriety'); this._hadAddiction = false; } if (this.addictionLevel >= 50) this._hadAddiction = true; // Настроение if (this.stats.hunger < 20) this.stats.mood = Math.max(0, this.stats.mood - 0.2); if (this.stats.warmth < 20) this.stats.mood = Math.max(0, this.stats.mood - 0.15); if (this.stats.health < 30) this.stats.mood = Math.max(0, this.stats.mood - 0.1); if (this.stats.hygiene < 30) this.stats.mood = Math.max(0, this.stats.mood - 0.1); // Звук урона if (this.stats.health < this._prevHealth - 1) { this.game.sound.playHurt(); } // Ачивка: сытость 100 if (this.stats.hunger >= 99) { this._wellFedTimer++; if (this._wellFedTimer >= 300) { this.game.achievements.check('well_fed'); } } else { this._wellFedTimer = 0; } } updateScreenEffects() { const dmg = document.getElementById('effect-damage'); const cold = document.getElementById('effect-cold'); const hunger = document.getElementById('effect-hunger'); const sleep = document.getElementById('effect-sleep'); if (this.stats.health < this._prevHealth - 1) { dmg.classList.add('active'); setTimeout(() => dmg.classList.remove('active'), 400); } if (this.stats.warmth < 25) { cold.classList.add('active'); } else { cold.classList.remove('active'); } if (this.stats.hunger < 15) { hunger.classList.add('active'); } else { hunger.classList.remove('active'); } if (this.isSleeping) { sleep.classList.add('active'); } else { sleep.classList.remove('active'); } const disease = document.getElementById('effect-disease'); if (disease) { if (this.isDiseased) { disease.classList.add('active'); } else { disease.classList.remove('active'); } } } checkInteractables() { let nearest = null; let nearestDist = Infinity; for (const obj of this.game.world.interactables) { const dist = this.position.distanceTo(obj.position); if (dist < obj.radius && dist < nearestDist) { nearest = obj; nearestDist = dist; } } // NPC for (const npc of this.game.npcManager.npcs) { const dist = this.position.distanceTo(npc.position); if (dist < 3 && dist < nearestDist) { nearest = { type: 'npc', npc: npc, position: npc.position, label: `Поговорить: ${npc.name}` }; nearestDist = dist; } } // Костёр this.isWarming = false; for (const obj of this.game.world.interactables) { if (obj.type === 'campfire') { const dist = this.position.distanceTo(obj.position); if (dist < obj.radius) { this.isWarming = true; } } } // Печка в укрытии if (this.game.housing.built && this.game.housing.upgrades.stove.built) { if (this.position.distanceTo(this.game.housing.position) < 5) { this.isWarming = true; } } this.nearestInteractable = nearest; this.game.ui.updateInteractionHint(nearest); } interact() { if (!this.nearestInteractable) return; const obj = this.nearestInteractable; switch (obj.type) { case 'dumpster': this.searchDumpster(obj); break; case 'bench': this.restOnBench(); break; case 'shop': this.game.interiors.enterBuilding('shop'); break; case 'shelter': this.sleep(); break; case 'campfire': this.game.notify('Вы греетесь у костра...'); break; case 'npc': this.game.npcManager.talkTo(obj.npc); break; case 'fountain': this.drinkFountain(); break; case 'trashpile': this.searchTrashPile(obj); break; case 'phone': this.usePhone(); break; case 'church': this.game.interiors.enterBuilding('church'); break; case 'jobboard': this.game.jobSystem.showJobBoard(); break; case 'player_shelter': this.game.housing.showMenu(); break; case 'camp_spot': this.game.housing.showMenu(); break; case 'hospital': this.game.interiors.enterBuilding('hospital'); break; case 'market': this.enterMarket(); break; // Интерьерные объекты case 'shop_counter': this.enterShop(); break; case 'hospital_desk': this.enterHospital(); break; case 'church_altar': this.enterChurch(); break; case 'exit_door': this.game.interiors.exitBuilding(); break; } } // === Попрошайничество === startBegging() { if (this.isBegging || this.begCooldown > 0) return; let nearNPC = false; for (const npc of this.game.npcManager.npcs) { if (this.position.distanceTo(npc.position) < 8) { nearNPC = true; break; } } if (!nearNPC) { this.game.notify('Рядом никого нет...'); return; } this.isBegging = true; this.begTimer = 0; this.game.ui.showBegProgress(true); } stopBegging() { if (!this.isBegging) return; this.isBegging = false; this.game.ui.showBegProgress(false); } updateBegging(dt) { if (!this.isBegging) return; this.begTimer += dt; this.game.ui.updateBegProgress(this.begTimer / this.begDuration); if (this.begTimer >= this.begDuration) { this.isBegging = false; this.begCooldown = 15; this.game.ui.showBegProgress(false); const begBonus = this.game.skills.getBegBonus() * this.game.reputation.getBegModifier(); this.game.skills.addXP('begging', 2); this.game.reputation.change(-1); const roll = Math.random(); if (roll < 0.35) { const amount = Math.floor((Math.random() * 20 + 5) * begBonus); this.stats.money += amount; this.game.sound.playCoin(); this.game.notify(`Вам дали ${amount} ₽!`, 'good'); this.game.questSystem.onEvent('find_money', amount); this.game.questSystem.onEvent('beg'); } else if (roll < 0.55) { this.game.inventory.addItem('bread', 1); this.game.sound.playPickup(); this.game.notify('Вам дали хлеб!', 'good'); this.game.questSystem.onEvent('find_food'); } else if (roll < 0.7) { this.game.notify('Прохожие игнорируют вас...'); this.stats.mood = Math.max(0, this.stats.mood - 3); } else if (roll < 0.85) { this.game.notify('"Иди работай!" — грубо ответили вам.'); this.stats.mood = Math.max(0, this.stats.mood - 5); } else { const amount = Math.floor(Math.random() * 50) + 20; this.stats.money += amount; this.game.sound.playCoin(); this.game.notify(`Щедрый прохожий дал ${amount} ₽!`, 'good'); this.game.questSystem.onEvent('find_money', amount); this.stats.mood = Math.min(100, this.stats.mood + 5); } } } // === Бускинг === startBusking() { if (this.isBusking || this.buskCooldown > 0 || this.isBegging) return; if (this.game.inventory.getCount('harmonica') <= 0) { this.game.notify('У вас нет гармошки!'); return; } // Ищем прохожих и NPC в радиусе 12 let nearCount = 0; for (const npc of this.game.npcManager.npcs) { if (this.position.distanceTo(npc.position) < 12) nearCount++; } if (this.game.npcManager.passersby) { for (const pb of this.game.npcManager.passersby) { if (pb.mesh) { const dist = this.position.distanceTo(pb.mesh.position); if (dist < 12) nearCount++; } } } if (nearCount === 0) { this.game.notify('Рядом некому играть...'); return; } this.isBusking = true; this.buskTimer = 0; this._buskNearCount = nearCount; this.game.ui.showBuskProgress(true); } stopBusking() { if (!this.isBusking) return; this.isBusking = false; this.game.ui.showBuskProgress(false); } updateBusking(dt) { if (!this.isBusking) return; this.buskTimer += dt; this.game.ui.updateBuskProgress(this.buskTimer / this.buskDuration); if (this.buskTimer >= this.buskDuration) { this.isBusking = false; this.buskCooldown = 20; this.game.ui.showBuskProgress(false); const begBonus = this.game.skills.getBegBonus(); const crowdMult = Math.min(2, 1 + (this._buskNearCount - 1) * 0.3); this.game.skills.addXP('begging', 1); const roll = Math.random(); if (roll < 0.50) { // Дали денег const amount = Math.floor((Math.random() * 20 + 10) * begBonus * crowdMult); this.stats.money += amount; this.game.sound.playCoin(); this.game.notify(`Прохожие оценили игру! +${amount} ₽`, 'good'); this.game.questSystem.onEvent('find_money', amount); this.game.questSystem.onEvent('busking'); } else if (roll < 0.75) { // Хорошие деньги const amount = Math.floor((Math.random() * 30 + 20) * begBonus * crowdMult); this.stats.money += amount; this.game.sound.playCoin(); this.game.notify(`Отличное выступление! +${amount} ₽`, 'good'); this.game.questSystem.onEvent('find_money', amount); this.game.questSystem.onEvent('busking'); this.stats.mood = Math.min(100, this.stats.mood + 5); } else if (roll < 0.90) { // Никто не обратил внимания this.game.notify('Прохожие не обратили внимания...'); } else { // Бонус к настроению this.stats.mood = Math.min(100, this.stats.mood + 10); const amount = Math.floor((Math.random() * 15 + 5) * begBonus * crowdMult); this.stats.money += amount; this.game.sound.playCoin(); this.game.notify(`Музыка подняла настроение! +${amount} ₽, +10 Настроение`, 'good'); this.game.questSystem.onEvent('find_money', amount); this.game.questSystem.onEvent('busking'); } } } // === Взаимодействия === searchDumpster(dumpster) { if (dumpster.searchCooldown > 0) { this.game.notify('Тут уже нечего искать. Подождите...'); return; } const scavBonus = this.game.skills.getScavengeBonus(); dumpster.searchCooldown = Math.floor(120 / scavBonus); this.game.sound.playPickup(); this.game.skills.addXP('scavenging', 1); this.stats.hygiene = Math.max(0, this.stats.hygiene - 5); const roll = Math.random(); if (roll < 0.18) { const count = Math.random() < 0.3 + scavBonus * 0.05 ? 2 : 1; this.game.inventory.addItem('bottle', count); this.game.notify(`Вы нашли бутылк${count > 1 ? 'и' : 'у'}!`, 'good'); this.game.questSystem.onEvent('collect_bottle'); if (count > 1) this.game.questSystem.onEvent('collect_bottle'); } else if (roll < 0.29) { this.game.inventory.addItem('bread', 1); this.game.notify('Вы нашли кусок хлеба!', 'good'); this.game.questSystem.onEvent('find_food'); } else if (roll < 0.38) { this.game.inventory.addItem('can', 1); this.game.notify('Вы нашли консервную банку!', 'good'); this.game.questSystem.onEvent('find_food'); } else if (roll < 0.47) { this.game.inventory.addItem('clothing', 1); this.game.notify('Вы нашли старую одежду!', 'good'); } else if (roll < 0.54) { this.game.inventory.addItem('newspaper', 1); this.game.notify('Вы нашли газету!'); } else if (roll < 0.62) { const coins = Math.floor(Math.random() * 15) + 1; this.stats.money += coins; this.game.sound.playCoin(); this.game.notify(`Вы нашли ${coins} ₽!`, 'good'); this.game.questSystem.onEvent('find_money', coins); } else if (roll < 0.70) { this.game.inventory.addItem('scrap', 1); this.game.notify('Вы нашли полезный хлам!', 'good'); } else if (roll < 0.77) { this.game.inventory.addItem('rope', 1); this.game.notify('Вы нашли верёвку!', 'good'); } else if (roll < 0.82) { // Шанс найти экипировку const eqItems = ['eq_old_hat', 'eq_old_jacket', 'eq_old_boots', 'eq_old_gloves']; const eqItem = eqItems[Math.floor(Math.random() * eqItems.length)]; this.game.inventory.addItem(eqItem, 1); this.game.notify(`Вы нашли ${this.game.inventory.itemData[eqItem].name}!`, 'good'); } else if (roll < 0.87) { this.game.inventory.addItem('apple', 1); this.game.notify('Вы нашли яблоко!', 'good'); } else { this.game.notify('Ничего полезного...'); this.stats.mood = Math.max(0, this.stats.mood - 3); } } restOnBench() { this.stats.mood = Math.min(100, this.stats.mood + 5); this.stats.health = Math.min(100, this.stats.health + 2); this.game.notify('Вы отдохнули на скамейке. +5 Настроение, +2 Здоровье.', 'good'); } drinkFountain() { this.game.ui.showDialog('Фонтанчик', 'Что хотите сделать?', [ 'Попить воды', 'Помыться', 'Уйти' ], (index) => { if (index === 0) { this.stats.hunger = Math.min(100, this.stats.hunger + 5); this.stats.mood = Math.min(100, this.stats.mood + 2); this.game.notify('Вы попили воды. +5 Сытость, +2 Настроение', 'good'); } else if (index === 1) { if (this.washCooldown > 0) { this.game.notify('Вы недавно мылись. Подождите...'); } else { this.stats.hygiene = Math.min(100, this.stats.hygiene + 40); this.washCooldown = 300; this.game.notify('+40 Гигиена', 'good'); this.game.questSystem.onEvent('wash'); } } this.game.ui.hideDialog(); }); } searchTrashPile(obj) { if (obj.searchCooldown > 0) { this.game.notify('Здесь уже нечего искать...'); return; } obj.searchCooldown = 90; this.game.skills.addXP('scavenging', 1); this.stats.hygiene = Math.max(0, this.stats.hygiene - 3); const roll = Math.random(); if (roll < 0.25) { this.game.inventory.addItem('bottle', 1); this.game.sound.playPickup(); this.game.notify('Нашли бутылку в мусоре!', 'good'); this.game.questSystem.onEvent('collect_bottle'); } else if (roll < 0.38) { const coins = Math.floor(Math.random() * 5) + 1; this.stats.money += coins; this.game.sound.playCoin(); this.game.notify(`Нашли ${coins} ₽!`, 'good'); } else if (roll < 0.50) { this.game.inventory.addItem('scrap', 1); this.game.sound.playPickup(); this.game.notify('Нашли полезный хлам!', 'good'); } else if (roll < 0.60) { this.game.inventory.addItem('candle', 1); this.game.sound.playPickup(); this.game.notify('Нашли свечу!', 'good'); } else if (roll < 0.68) { this.game.inventory.addItem('rope', 1); this.game.sound.playPickup(); this.game.notify('Нашли верёвку!', 'good'); } else { this.game.notify('Мусор как мусор...'); } } usePhone() { this.game.ui.showDialog('Таксофон', 'Позвонить на горячую линию помощи?', [ 'Позвонить (бесплатно)', 'Не нужно' ], (index) => { if (index === 0) { this.stats.mood = Math.min(100, this.stats.mood + 10); this.game.notify('Вам дали полезные советы. +10 Настроение', 'good'); } this.game.ui.hideDialog(); }); } enterShop() { const items = [ { name: 'Хлеб', key: 'bread', price: 30, desc: '+20 Сытость' }, { name: 'Консервы', key: 'can', price: 50, desc: '+35 Сытость' }, { name: 'Чай', key: 'tea', price: 20, desc: '+20 Тепло, +5 Настроение' }, { name: 'Бинт', key: 'bandage', price: 40, desc: '+25 Здоровье' }, { name: 'Свеча', key: 'candle', price: 15, desc: 'Для крафта' }, { name: 'Верёвка', key: 'rope', price: 25, desc: 'Для крафта' }, { name: 'Яблоко', key: 'apple', price: 15, desc: '+10 Сытость, +5 Здоровье' }, { name: 'Мыло', key: 'soap', price: 20, desc: '+30 Гигиена' }, { name: 'Перчатки', key: 'eq_gloves', price: 60, desc: 'Экипировка: +8 Тепло' }, { name: 'Ботинки', key: 'eq_boots', price: 80, desc: 'Экипировка: +10 Тепло' }, ]; const discount = this.game.skills.getTradeDiscount() * this.game.reputation.getShopModifier(); const choices = items.map(item => { const finalPrice = Math.floor(item.price * discount); return `${item.name} — ${finalPrice} ₽ (${item.desc})`; }); choices.push('Сдать бутылки (5₽/шт)'); choices.push('Уйти'); this.game.sound.playDialogOpen(); this.game.ui.showDialog('Продавец', 'Чего желаете?', choices, (index) => { if (index < items.length) { const item = items[index]; const finalPrice = Math.floor(item.price * discount); if (this.stats.money >= finalPrice) { this.stats.money -= finalPrice; this.game.skills.addXP('trading', 1); this.game.inventory.addItem(item.key, 1); this.game.sound.playCoin(); this.game.notify(`Куплено: ${item.name}`, 'good'); } else { this.game.notify('Не хватает денег!', 'bad'); } } else if (index === items.length) { const bottles = this.game.inventory.getCount('bottle'); if (bottles > 0) { const earnings = bottles * 5; this.stats.money += earnings; this.game.inventory.removeItem('bottle', bottles); this.game.sound.playCoin(); this.game.notify(`Сдали ${bottles} бутылок за ${earnings} ₽!`, 'good'); this.game.questSystem.onEvent('sell_bottles', bottles); this.game.totalBottlesSold += bottles; if (this.game.totalBottlesSold >= 20) { this.game.achievements.check('bottle_king'); } } else { this.game.notify('У вас нет бутылок.'); } } this.game.ui.hideDialog(); }); } enterHospital() { const choices = ['Осмотр (бесплатно)', 'Лечение (50₽)', 'Уйти']; this.game.sound.playDialogOpen(); this.game.ui.showDialog('Больница', 'Добро пожаловать. Чем помочь?', choices, (index) => { if (index === 0) { this.stats.health = Math.min(100, this.stats.health + 15); if (this.isDiseased) { this.isDiseased = false; this.diseaseTimer = 0; this.game.notify('+15 Здоровье. Болезнь вылечена!', 'good'); } else { this.game.notify('+15 Здоровье. Вас осмотрели.', 'good'); } } else if (index === 1) { if (this.stats.money >= 50) { this.stats.money -= 50; this.stats.health = 100; this.isDiseased = false; this.diseaseTimer = 0; this.game.sound.playCoin(); this.game.notify('Полное здоровье восстановлено! Болезнь вылечена!', 'good'); } else { this.game.notify('Не хватает денег!', 'bad'); } } this.game.ui.hideDialog(); this.game.questSystem.onEvent('visit_hospital'); }); } enterMarket() { const items = [ { name: 'Рыба', key: 'fish', price: 25, desc: '+25 Сытость' }, { name: 'Пальто', key: 'eq_coat', price: 120, desc: 'Экипировка: +18 Тепло' }, { name: 'Тёплые сапоги', key: 'eq_warm_boots', price: 100, desc: 'Экипировка: +18 Тепло' }, { name: 'Жилетка', key: 'eq_vest', price: 150, desc: 'Экипировка: +10 Защита' }, { name: 'Каска', key: 'eq_helmet', price: 90, desc: 'Экипировка: +8 Защита' }, { name: 'Водка', key: 'vodka', price: 40, desc: '+30 Тепло, -10 Здоровье' }, ]; const discount = this.game.skills.getTradeDiscount() * this.game.reputation.getShopModifier(); const choices = items.map(item => { const finalPrice = Math.floor(item.price * discount); return `${item.name} — ${finalPrice} ₽ (${item.desc})`; }); choices.push('Уйти'); this.game.sound.playDialogOpen(); this.game.ui.showDialog('Рынок', 'Здесь можно найти полезные вещи.', choices, (index) => { if (index < items.length) { const item = items[index]; const finalPrice = Math.floor(item.price * discount); if (this.stats.money >= finalPrice) { this.stats.money -= finalPrice; this.game.skills.addXP('trading', 2); this.game.inventory.addItem(item.key, 1); this.game.sound.playCoin(); this.game.notify(`Куплено: ${item.name}`, 'good'); } else { this.game.notify('Не хватает денег!', 'bad'); } } this.game.ui.hideDialog(); }); } sleep() { if (this.stats.hunger < 10) { this.game.notify('Слишком голодно, не уснуть...', 'bad'); return; } this.isSleeping = true; this.sleepTimer = 0; this._shelterSleep = false; this.game.notify('Вы легли спать...'); } updateSleep(dt) { this.sleepTimer += dt; if (this.sleepTimer >= 5) { this.isSleeping = false; let bonus; if (this._shelterSleep) { bonus = this.game.housing.getSleepBonus(); } else { bonus = { health: 15, mood: 10, warmth: 20 }; } this.stats.health = Math.min(100, this.stats.health + bonus.health); this.stats.mood = Math.min(100, this.stats.mood + bonus.mood); this.stats.warmth = Math.min(100, this.stats.warmth + bonus.warmth); this.stats.hunger = Math.max(0, this.stats.hunger - 15); this.game.gameTime += 360; this._shelterSleep = false; const bonusText = this._shelterSleep ? ' (улучшенный отдых)' : ''; this.game.notify(`Вы проснулись отдохнувшим.${bonusText}`, 'good'); this.game.questSystem.onEvent('sleep'); } } checkDeath() { if (this.stats.health <= 0) { let reason = 'Вы не выдержали тяжёлой жизни на улице.'; if (this.stats.hunger <= 0) reason = 'Вы умерли от голода.'; if (this.stats.warmth <= 0) reason = 'Вы замёрзли насмерть.'; this.game.gameOver(reason); } } useItem(itemKey) { const effects = { bread: () => { this.stats.hunger = Math.min(100, this.stats.hunger + 20); this.game.sound.playEat(); this.game.notify('+20 Сытость', 'good'); }, can: () => { this.stats.hunger = Math.min(100, this.stats.hunger + 35); this.game.sound.playEat(); this.game.notify('+35 Сытость', 'good'); }, tea: () => { this.stats.warmth = Math.min(100, this.stats.warmth + 20); this.stats.mood += 5; this.game.sound.playEat(); this.game.notify('+20 Тепло, +5 Настроение', 'good'); }, bandage: () => { this.stats.health = Math.min(100, this.stats.health + 25); this.game.notify('+25 Здоровье', 'good'); }, clothing: () => { this.stats.warmth = Math.min(100, this.stats.warmth + 30); this.game.notify('+30 Тепло', 'good'); }, newspaper: () => { this.stats.mood = Math.min(100, this.stats.mood + 5); this.game.notify('Вы почитали газету. +5 Настроение', 'good'); }, medkit: () => { this.stats.health = Math.min(100, this.stats.health + 50); if (this.isDiseased) { this.isDiseased = false; this.diseaseTimer = 0; this.game.notify('+50 Здоровье. Болезнь вылечена!', 'good'); } else { this.game.notify('+50 Здоровье', 'good'); } }, stew: () => { this.stats.hunger = Math.min(100, this.stats.hunger + 50); this.stats.warmth = Math.min(100, this.stats.warmth + 15); this.game.sound.playEat(); this.game.notify('+50 Сытость, +15 Тепло', 'good'); }, blanket: () => { this.stats.warmth = Math.min(100, this.stats.warmth + 50); this.stats.mood = Math.min(100, this.stats.mood + 10); this.game.notify('+50 Тепло, +10 Настроение', 'good'); }, soap: () => { this.stats.hygiene = Math.min(100, this.stats.hygiene + 30); this.game.notify('+30 Гигиена', 'good'); }, harmonica: () => { // Если рядом есть люди — начать бускинг, иначе просто играть для себя let hasNearby = false; for (const npc of this.game.npcManager.npcs) { if (this.position.distanceTo(npc.position) < 12) { hasNearby = true; break; } } if (!hasNearby && this.game.npcManager.passersby) { for (const pb of this.game.npcManager.passersby) { if (pb.mesh && this.position.distanceTo(pb.mesh.position) < 12) { hasNearby = true; break; } } } if (hasNearby && this.buskCooldown <= 0 && !this.isBusking) { this.startBusking(); } else { this.stats.mood = Math.min(100, this.stats.mood + 20); this.game.notify('Вы сыграли мелодию. +20 Настроение', 'good'); } }, fish: () => { this.stats.hunger = Math.min(100, this.stats.hunger + 25); this.game.sound.playEat(); this.game.notify('+25 Сытость', 'good'); }, apple: () => { this.stats.hunger = Math.min(100, this.stats.hunger + 10); this.stats.health = Math.min(100, this.stats.health + 5); this.game.sound.playEat(); this.game.notify('+10 Сытость, +5 Здоровье', 'good'); }, vodka: () => { this.stats.warmth = Math.min(100, this.stats.warmth + (this.addictionLevel > 60 ? 40 : 30)); this.stats.health = Math.max(0, this.stats.health - 10); const moodGain = this.addictionLevel > 80 ? 0 : 15; if (moodGain > 0) this.stats.mood = Math.min(100, this.stats.mood + moodGain); this.addictionLevel = Math.min(100, this.addictionLevel + 20); this.lastDrinkTime = this.game.gameTime + (this.game.gameDay - 1) * 24 * 60; this.withdrawalTimer = 0; const msg = this.addictionLevel > 80 ? '+40 Тепло, -10 Здоровье (привычка...)' : this.addictionLevel > 60 ? '+40 Тепло, -10 Здоровье, +15 Настроение (толерантность)' : '+30 Тепло, -10 Здоровье, +15 Настроение'; this.game.notify(msg, 'good'); }, vitamins: () => { this.stats.health = Math.min(100, this.stats.health + 15); this.stats.mood = Math.min(100, this.stats.mood + 10); this.game.notify('+15 Здоровье, +10 Настроение', 'good'); }, torch: () => { this.stats.warmth = Math.min(100, this.stats.warmth + 20); if (this.game.isNight()) { this.stats.mood = Math.min(100, this.stats.mood + 10); this.game.notify('+20 Тепло, +10 Настроение', 'good'); } else { this.game.notify('+20 Тепло', 'good'); } }, }; if (effects[itemKey]) { effects[itemKey](); if (itemKey !== 'harmonica') { this.game.inventory.removeItem(itemKey, 1); } return true; } return false; } enterChurch() { this.game.ui.showDialog('Церковь', 'Тёплое помещение. Тихо играет органная музыка. Добрая женщина предлагает помощь.', [ 'Попросить еду', 'Попросить одежду', 'Помолиться', 'Уйти' ], (index) => { if (index === 0) { this.game.inventory.addItem('bread', 2); this.game.inventory.addItem('tea', 1); this.stats.warmth = Math.min(100, this.stats.warmth + 10); this.game.sound.playPickup(); this.game.notify('Вам дали хлеб и чай. +10 Тепло', 'good'); } else if (index === 1) { this.game.inventory.addItem('clothing', 1); this.stats.warmth = Math.min(100, this.stats.warmth + 10); this.game.sound.playPickup(); this.game.notify('Вам дали тёплую одежду. +10 Тепло', 'good'); } else if (index === 2) { this.stats.mood = Math.min(100, this.stats.mood + 20); this.stats.health = Math.min(100, this.stats.health + 5); this.game.notify('Вы чувствуете покой. +20 Настроение, +5 Здоровье', 'good'); } this.game.ui.hideDialog(); this.game.questSystem.onEvent('visit_church'); this.game.reputation.change(2); }); } trackLocations() { const cfg = this.game.world.mapConfig?.structures || {}; const locs = { shop: { x: cfg.shop?.x ?? -25, z: cfg.shop?.z ?? -12, r: 8 }, church: { x: cfg.church?.x ?? 30, z: cfg.church?.z ?? 60, r: 10 }, park: { x: cfg.park?.x ?? -30, z: cfg.park?.z ?? 25, r: cfg.park?.radius ?? 18 }, shelter: { x: cfg.shelter?.x ?? -35, z: cfg.shelter?.z ?? 35, r: 6 }, construction: { x: cfg.construction?.x ?? 70, z: cfg.construction?.z ?? 60, r: cfg.construction?.radius ?? 8 }, hospital: { x: cfg.hospital?.x ?? -45, z: cfg.hospital?.z ?? -55, r: 8 }, market: { x: cfg.market?.x ?? 35, z: cfg.market?.z ?? -55, r: 8 }, busstop: { x: cfg.busStop?.x ?? -20, z: cfg.busStop?.z ?? 7, r: 5 }, }; for (const [name, loc] of Object.entries(locs)) { const dx = this.position.x - loc.x; const dz = this.position.z - loc.z; if (Math.sqrt(dx * dx + dz * dz) < loc.r) { this.game.visitedLocations.add(name); } } if (this.game.visitedLocations.size >= 8) { this.game.achievements.check('explorer'); } } checkDanger() { return this.game.dangers.hasNearbyDanger(); } }