Initial commit: 3D Hommie RPG game
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
352
js/game/Housing.js
Normal file
352
js/game/Housing.js
Normal file
@@ -0,0 +1,352 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user