353 lines
13 KiB
JavaScript
353 lines
13 KiB
JavaScript
import * as THREE from 'three';
|
||
|
||
export class Housing {
|
||
constructor(game) {
|
||
this.game = game;
|
||
this.built = false;
|
||
this.mesh = null;
|
||
|
||
this.position = new THREE.Vector3(-20, 0, 38);
|
||
|
||
// Улучшения
|
||
this.upgrades = {
|
||
roof: { built: false, name: 'Крыша', icon: '🏚️', desc: 'Защита от дождя и снега', cost: { scrap: 3, rope: 1 }, effect: 'weather_protection' },
|
||
bed: { built: false, name: 'Лежанка', icon: '🛏️', desc: 'Лучший отдых при сне (+10 здоровье)', cost: { clothing: 2, newspaper: 3 }, effect: 'better_sleep' },
|
||
stove: { built: false, name: 'Печка', icon: '🔥', desc: 'Обогрев и готовка', cost: { scrap: 4, candle: 2 }, effect: 'heating' },
|
||
door: { built: false, name: 'Дверь', icon: '🚪', desc: 'Защита от врагов в лагере', cost: { scrap: 2, rope: 2 }, effect: 'safety' },
|
||
storage: { built: false, name: 'Тайник', icon: '📦', desc: '+10 слотов инвентаря', cost: { scrap: 3, rope: 1 }, effect: 'extra_storage' },
|
||
};
|
||
}
|
||
|
||
initFromConfig() {
|
||
const cfg = this.game.world.mapConfig?.structures?.campSpot || {};
|
||
this.position.set(cfg.x ?? -20, 0, cfg.z ?? 38);
|
||
}
|
||
|
||
buildShelter() {
|
||
if (this.built) return;
|
||
|
||
// Нужно 5 хлама и 2 верёвки
|
||
const inv = this.game.inventory;
|
||
if (inv.getCount('scrap') < 5 || inv.getCount('rope') < 2) {
|
||
this.game.notify('Нужно: 5x Хлам, 2x Верёвка', 'bad');
|
||
return;
|
||
}
|
||
|
||
inv.removeItem('scrap', 5);
|
||
inv.removeItem('rope', 2);
|
||
this.built = true;
|
||
this.createMesh();
|
||
this.addInteractable();
|
||
this.game.notify('Вы построили своё укрытие!', 'good');
|
||
this.game.sound.playQuestComplete();
|
||
this.game.questSystem.onEvent('build_shelter');
|
||
this.game.achievements.check('shelter_built');
|
||
this.game.skills.addXP('survival', 5);
|
||
}
|
||
|
||
createMesh() {
|
||
if (this.mesh) {
|
||
this.game.scene.remove(this.mesh);
|
||
}
|
||
|
||
const group = new THREE.Group();
|
||
|
||
// Базовый каркас — палатка из палок
|
||
const frameMat = new THREE.MeshStandardMaterial({ color: 0x5c3a1e });
|
||
|
||
// Основание
|
||
const base = new THREE.Mesh(
|
||
new THREE.BoxGeometry(4, 0.15, 3),
|
||
new THREE.MeshStandardMaterial({ color: 0x4a3a2a })
|
||
);
|
||
base.position.y = 0.075;
|
||
base.receiveShadow = true;
|
||
group.add(base);
|
||
|
||
// Стойки
|
||
const stickGeo = new THREE.CylinderGeometry(0.06, 0.06, 2.2, 6);
|
||
[[-1.8, 1.1, -1.3], [1.8, 1.1, -1.3], [-1.8, 1.1, 1.3], [1.8, 1.1, 1.3]].forEach(([x, y, z]) => {
|
||
const stick = new THREE.Mesh(stickGeo, frameMat);
|
||
stick.position.set(x, y, z);
|
||
stick.castShadow = true;
|
||
group.add(stick);
|
||
});
|
||
|
||
// Перекладины
|
||
const barGeo = new THREE.CylinderGeometry(0.05, 0.05, 3.8, 6);
|
||
const bar1 = new THREE.Mesh(barGeo, frameMat);
|
||
bar1.position.set(0, 2.2, -1.3);
|
||
bar1.rotation.z = Math.PI / 2;
|
||
group.add(bar1);
|
||
|
||
const bar2 = new THREE.Mesh(barGeo, frameMat);
|
||
bar2.position.set(0, 2.2, 1.3);
|
||
bar2.rotation.z = Math.PI / 2;
|
||
group.add(bar2);
|
||
|
||
// Крыша (если построена)
|
||
if (this.upgrades.roof.built) {
|
||
const roofMat = new THREE.MeshStandardMaterial({ color: 0x3a5a3a, side: THREE.DoubleSide });
|
||
const roofGeo = new THREE.PlaneGeometry(4.2, 3.2);
|
||
const roof = new THREE.Mesh(roofGeo, roofMat);
|
||
roof.position.set(0, 2.3, 0);
|
||
roof.rotation.x = -Math.PI / 2;
|
||
roof.castShadow = true;
|
||
roof.receiveShadow = true;
|
||
group.add(roof);
|
||
}
|
||
|
||
// Лежанка
|
||
if (this.upgrades.bed.built) {
|
||
const bedMat = new THREE.MeshStandardMaterial({ color: 0x6b5b4b });
|
||
const bed = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.2, 2.2), bedMat);
|
||
bed.position.set(-0.5, 0.25, 0);
|
||
group.add(bed);
|
||
|
||
const pillowMat = new THREE.MeshStandardMaterial({ color: 0x7b6b5b });
|
||
const pillow = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.15, 0.4), pillowMat);
|
||
pillow.position.set(-0.5, 0.4, -0.8);
|
||
group.add(pillow);
|
||
}
|
||
|
||
// Печка
|
||
if (this.upgrades.stove.built) {
|
||
const stoveMat = new THREE.MeshStandardMaterial({ color: 0x555555 });
|
||
const stove = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.4, 0.5, 8), stoveMat);
|
||
stove.position.set(1.2, 0.25, 0.5);
|
||
stove.castShadow = true;
|
||
group.add(stove);
|
||
|
||
// Огонь
|
||
const fireMat = new THREE.MeshStandardMaterial({
|
||
color: 0xff6600,
|
||
emissive: 0xff4400,
|
||
emissiveIntensity: 0.8
|
||
});
|
||
const fire = new THREE.Mesh(new THREE.SphereGeometry(0.15, 6, 4), fireMat);
|
||
fire.position.set(1.2, 0.55, 0.5);
|
||
group.add(fire);
|
||
}
|
||
|
||
// Дверь
|
||
if (this.upgrades.door.built) {
|
||
const doorMat = new THREE.MeshStandardMaterial({ color: 0x5c4a3a });
|
||
const door = new THREE.Mesh(new THREE.BoxGeometry(1.2, 2, 0.1), doorMat);
|
||
door.position.set(0, 1, 1.35);
|
||
door.castShadow = true;
|
||
group.add(door);
|
||
}
|
||
|
||
// Тайник
|
||
if (this.upgrades.storage.built) {
|
||
const boxMat = new THREE.MeshStandardMaterial({ color: 0x4a4a3a });
|
||
const box = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.6, 0.6), boxMat);
|
||
box.position.set(1.2, 0.3, -0.7);
|
||
box.castShadow = true;
|
||
group.add(box);
|
||
}
|
||
|
||
group.position.copy(this.position);
|
||
this.game.scene.add(group);
|
||
this.mesh = group;
|
||
}
|
||
|
||
addInteractable() {
|
||
// Убираем старое если есть
|
||
this.game.world.interactables = this.game.world.interactables.filter(o => o.type !== 'player_shelter');
|
||
|
||
this.game.world.interactables.push({
|
||
position: this.position.clone(),
|
||
radius: 4,
|
||
type: 'player_shelter',
|
||
label: 'Ваше укрытие'
|
||
});
|
||
}
|
||
|
||
showMenu() {
|
||
if (!this.built) {
|
||
// Предложить построить
|
||
const hasRes = this.game.inventory.getCount('scrap') >= 5 && this.game.inventory.getCount('rope') >= 2;
|
||
this.game.ui.showDialog('Место для лагеря', 'Здесь можно поставить укрытие. Нужно: 5x Хлам, 2x Верёвка.', [
|
||
hasRes ? 'Построить укрытие' : 'Не хватает материалов',
|
||
'Уйти'
|
||
], (i) => {
|
||
if (i === 0 && hasRes) {
|
||
this.buildShelter();
|
||
}
|
||
this.game.ui.hideDialog();
|
||
});
|
||
return;
|
||
}
|
||
|
||
const choices = [];
|
||
const actions = [];
|
||
|
||
// Спать (если есть лежанка — лучше)
|
||
choices.push(this.upgrades.bed.built ? 'Поспать (улучшенный отдых)' : 'Поспать');
|
||
actions.push('sleep');
|
||
|
||
// Погреться (если есть печка)
|
||
if (this.upgrades.stove.built) {
|
||
choices.push('Погреться у печки');
|
||
actions.push('warm');
|
||
}
|
||
|
||
// Доступные улучшения
|
||
for (const [key, upg] of Object.entries(this.upgrades)) {
|
||
if (upg.built) continue;
|
||
const canBuild = this.canBuildUpgrade(key);
|
||
const costStr = Object.entries(upg.cost).map(([item, count]) => {
|
||
const name = this.game.inventory.itemData[item]?.name || item;
|
||
return `${count}x ${name}`;
|
||
}).join(', ');
|
||
choices.push(`${upg.icon} ${upg.name} (${costStr})${canBuild ? '' : ' [не хватает]'}`);
|
||
actions.push('upgrade_' + key);
|
||
}
|
||
|
||
choices.push('Уйти');
|
||
actions.push('leave');
|
||
|
||
this.game.sound.playDialogOpen();
|
||
this.game.ui.showDialog('Ваше укрытие', 'Что хотите сделать?', choices, (index) => {
|
||
const action = actions[index];
|
||
if (action === 'sleep') {
|
||
this.sleepHere();
|
||
} else if (action === 'warm') {
|
||
this.warmHere();
|
||
} else if (action && action.startsWith('upgrade_')) {
|
||
const key = action.replace('upgrade_', '');
|
||
this.buildUpgrade(key);
|
||
}
|
||
this.game.ui.hideDialog();
|
||
});
|
||
}
|
||
|
||
canBuildUpgrade(key) {
|
||
const upg = this.upgrades[key];
|
||
if (!upg || upg.built) return false;
|
||
for (const [item, count] of Object.entries(upg.cost)) {
|
||
if (this.game.inventory.getCount(item) < count) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
buildUpgrade(key) {
|
||
if (!this.canBuildUpgrade(key)) {
|
||
this.game.notify('Не хватает материалов!', 'bad');
|
||
return;
|
||
}
|
||
|
||
const upg = this.upgrades[key];
|
||
for (const [item, count] of Object.entries(upg.cost)) {
|
||
this.game.inventory.removeItem(item, count);
|
||
}
|
||
|
||
upg.built = true;
|
||
this.game.notify(`Построено: ${upg.name}!`, 'good');
|
||
this.game.sound.playQuestComplete();
|
||
this.game.skills.addXP('survival', 3);
|
||
|
||
// Применить эффект тайника
|
||
if (key === 'storage') {
|
||
this.game.inventory.maxSlots += 10;
|
||
this.game.notify('+10 слотов инвентаря!', 'good');
|
||
}
|
||
|
||
// Перестроить меш
|
||
this.createMesh();
|
||
}
|
||
|
||
sleepHere() {
|
||
const player = this.game.player;
|
||
if (player.stats.hunger < 10) {
|
||
this.game.notify('Слишком голодно, не уснуть...', 'bad');
|
||
return;
|
||
}
|
||
|
||
player.isSleeping = true;
|
||
player.sleepTimer = 0;
|
||
player._shelterSleep = true; // Помечаем что сон в укрытии
|
||
this.game.notify('Вы легли спать в укрытии...');
|
||
}
|
||
|
||
warmHere() {
|
||
this.game.player.stats.warmth = Math.min(100, this.game.player.stats.warmth + 30);
|
||
this.game.player.stats.mood = Math.min(100, this.game.player.stats.mood + 5);
|
||
this.game.notify('+30 Тепло, +5 Настроение', 'good');
|
||
}
|
||
|
||
// Модификатор сна в укрытии
|
||
getSleepBonus() {
|
||
let bonus = { health: 15, mood: 10, warmth: 20 };
|
||
if (this.upgrades.bed.built) {
|
||
bonus.health += 10;
|
||
bonus.mood += 5;
|
||
}
|
||
if (this.upgrades.roof.built) {
|
||
bonus.warmth += 15;
|
||
}
|
||
if (this.upgrades.stove.built) {
|
||
bonus.warmth += 10;
|
||
}
|
||
return bonus;
|
||
}
|
||
|
||
// Враги не атакуют рядом с укрытием если есть дверь
|
||
isSafeZone(pos) {
|
||
if (!this.built || !this.upgrades.door.built) return false;
|
||
return pos.distanceTo(this.position) < 6;
|
||
}
|
||
|
||
// Крыша защищает от погоды
|
||
hasRoof() {
|
||
return this.built && this.upgrades.roof.built;
|
||
}
|
||
|
||
isPlayerInShelter() {
|
||
if (!this.built) return false;
|
||
return this.game.player.position.distanceTo(this.position) < 5;
|
||
}
|
||
|
||
getSaveData() {
|
||
const upgradeData = {};
|
||
for (const [key, upg] of Object.entries(this.upgrades)) {
|
||
upgradeData[key] = upg.built;
|
||
}
|
||
return {
|
||
built: this.built,
|
||
upgrades: upgradeData
|
||
};
|
||
}
|
||
|
||
loadSaveData(data) {
|
||
if (!data) return;
|
||
this.built = data.built || false;
|
||
if (data.upgrades) {
|
||
for (const [key, built] of Object.entries(data.upgrades)) {
|
||
if (this.upgrades[key]) {
|
||
this.upgrades[key].built = built;
|
||
}
|
||
}
|
||
}
|
||
if (this.built) {
|
||
this.createMesh();
|
||
this.addInteractable();
|
||
if (this.upgrades.storage.built) {
|
||
this.game.inventory.maxSlots = 30;
|
||
}
|
||
}
|
||
}
|
||
|
||
reset() {
|
||
if (this.mesh) {
|
||
this.game.scene.remove(this.mesh);
|
||
this.mesh = null;
|
||
}
|
||
this.built = false;
|
||
for (const upg of Object.values(this.upgrades)) {
|
||
upg.built = false;
|
||
}
|
||
}
|
||
}
|