Initial commit: 3D Hommie RPG game

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-02-25 01:04:09 +03:00
commit fb5f09212b
34 changed files with 14550 additions and 0 deletions

352
js/game/Housing.js Normal file
View 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;
}
}
}