Files
Hommie_RPG_Game/js/game/Player.js
Maxim Dolgolyov fb5f09212b Initial commit: 3D Hommie RPG game
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:04:09 +03:00

1175 lines
48 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}