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

523
js/game/Interiors.js Normal file
View File

@@ -0,0 +1,523 @@
import * as THREE from 'three';
export class Interiors {
constructor(game) {
this.game = game;
this.isInside = false;
this.currentBuilding = null;
this.savedPosition = null;
this.interiorObjects = [];
this.interiorColliders = [];
this.interiorInteractables = [];
this.built = false;
}
buildInteriors() {
if (this.built) return;
this.built = true;
this.buildShop();
this.buildHospital();
this.buildChurch();
}
createInteriorNPC(group, x, y, z, bodyColor, hasApron) {
// Тело
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor });
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.32, 1.0, 8), bodyMat);
body.position.set(x, 0.8, z);
body.castShadow = true;
group.add(body);
// Голова
const head = new THREE.Mesh(
new THREE.SphereGeometry(0.2, 8, 6),
new THREE.MeshStandardMaterial({ color: 0xd4a574 })
);
head.position.set(x, 1.45, z);
group.add(head);
// Ноги
const legMat = new THREE.MeshStandardMaterial({ color: 0x333344 });
[-0.1, 0.1].forEach(side => {
const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.07, 0.4, 6), legMat);
leg.position.set(x + side, 0.2, z);
group.add(leg);
});
// Фартук (для продавца)
if (hasApron) {
const apron = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.6, 0.05),
new THREE.MeshStandardMaterial({ color: 0xeeeeee })
);
apron.position.set(x, 0.7, z + 0.28);
group.add(apron);
}
}
addDoorFrame(group, ox, oz, d) {
const doorMat = new THREE.MeshStandardMaterial({ color: 0x6b3a1f });
// Дверная рама (ширина проёма 2.4)
const frameLeft = new THREE.Mesh(new THREE.BoxGeometry(0.12, 2.4, 0.25), doorMat);
frameLeft.position.set(ox - 1.2, 1.2, oz + d / 2);
group.add(frameLeft);
const frameRight = new THREE.Mesh(new THREE.BoxGeometry(0.12, 2.4, 0.25), doorMat);
frameRight.position.set(ox + 1.2, 1.2, oz + d / 2);
group.add(frameRight);
const frameTop = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.12, 0.25), doorMat);
frameTop.position.set(ox, 2.4, oz + d / 2);
group.add(frameTop);
// Табличка "ВЫХОД" (зелёная, светящаяся)
const signMat = new THREE.MeshStandardMaterial({ color: 0x22cc44, emissive: 0x22cc44, emissiveIntensity: 0.8 });
const sign = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.3, 0.05), signMat);
sign.position.set(ox, 2.7, oz + d / 2 - 0.05);
group.add(sign);
// Подсветка у двери
const doorLight = new THREE.PointLight(0x44ff66, 0.3, 4);
doorLight.position.set(ox, 2.5, oz + d / 2 - 0.5);
group.add(doorLight);
// Коврик у двери
const matFloor = new THREE.Mesh(
new THREE.PlaneGeometry(2, 1),
new THREE.MeshStandardMaterial({ color: 0x886644 })
);
matFloor.rotation.x = -Math.PI / 2;
matFloor.position.set(ox, 0.02, oz + d / 2 - 0.5);
group.add(matFloor);
}
buildShop() {
const ox = 500, oz = 0;
const w = 10, d = 8, h = 3.5;
const group = new THREE.Group();
// Пол
const floorMat = new THREE.MeshStandardMaterial({ color: 0xddccaa });
const floor = new THREE.Mesh(new THREE.PlaneGeometry(w, d), floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(ox, 0.01, oz);
floor.receiveShadow = true;
group.add(floor);
// Потолок
const ceilMat = new THREE.MeshStandardMaterial({ color: 0xeeeeee, side: THREE.BackSide });
const ceil = new THREE.Mesh(new THREE.PlaneGeometry(w, d), ceilMat);
ceil.rotation.x = -Math.PI / 2;
ceil.position.set(ox, h, oz);
group.add(ceil);
// Стены
const wallMat = new THREE.MeshStandardMaterial({ color: 0xf5e6cc });
// Задняя стена
const backWall = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.2), wallMat);
backWall.position.set(ox, h / 2, oz - d / 2);
backWall.castShadow = true;
group.add(backWall);
// Левая стена
const leftWall = new THREE.Mesh(new THREE.BoxGeometry(0.2, h, d), wallMat);
leftWall.position.set(ox - w / 2, h / 2, oz);
group.add(leftWall);
// Правая стена
const rightWall = new THREE.Mesh(new THREE.BoxGeometry(0.2, h, d), wallMat);
rightWall.position.set(ox + w / 2, h / 2, oz);
group.add(rightWall);
// Передняя стена с проёмом (дверь)
const frontLeft = new THREE.Mesh(new THREE.BoxGeometry(w / 2 - 1.4, h, 0.2), wallMat);
frontLeft.position.set(ox - w / 4 - 0.7, h / 2, oz + d / 2);
group.add(frontLeft);
const frontRight = new THREE.Mesh(new THREE.BoxGeometry(w / 2 - 1.4, h, 0.2), wallMat);
frontRight.position.set(ox + w / 4 + 0.7, h / 2, oz + d / 2);
group.add(frontRight);
// Прилавок
const counterMat = new THREE.MeshStandardMaterial({ color: 0x8b6914 });
const counter = new THREE.Mesh(new THREE.BoxGeometry(5, 1, 0.6), counterMat);
counter.position.set(ox, 0.5, oz - 1.5);
counter.castShadow = true;
group.add(counter);
// Продавец за прилавком
this.createInteriorNPC(group, ox + 1, 0, oz - 2.2, 0x336633, true);
// Полки на стене
const shelfMat = new THREE.MeshStandardMaterial({ color: 0x9b7b3c });
for (let i = 0; i < 3; i++) {
const shelf = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.08, 0.5), shelfMat);
shelf.position.set(ox - 3 + i * 3, 1.5, oz - d / 2 + 0.35);
group.add(shelf);
// Товары на полках
for (let j = 0; j < 3; j++) {
const item = new THREE.Mesh(
new THREE.BoxGeometry(0.3, 0.4, 0.3),
new THREE.MeshStandardMaterial({ color: [0xe8a030, 0x4488cc, 0xcc4444][j] })
);
item.position.set(ox - 3.5 + i * 3 + j * 0.5, 1.75, oz - d / 2 + 0.35);
group.add(item);
}
}
// Дверная рама и табличка
this.addDoorFrame(group, ox, oz, d);
// Свет
const light = new THREE.PointLight(0xffe8c0, 1, 15);
light.position.set(ox, h - 0.3, oz);
group.add(light);
this.game.scene.add(group);
this.interiorObjects.push(group);
// Коллайдеры
this.interiorColliders.push(
new THREE.Box3(new THREE.Vector3(ox - w / 2 - 0.1, 0, oz - d / 2 - 0.2), new THREE.Vector3(ox + w / 2 + 0.1, h, oz - d / 2)),
new THREE.Box3(new THREE.Vector3(ox - w / 2 - 0.2, 0, oz - d / 2), new THREE.Vector3(ox - w / 2, h, oz + d / 2)),
new THREE.Box3(new THREE.Vector3(ox + w / 2, 0, oz - d / 2), new THREE.Vector3(ox + w / 2 + 0.2, h, oz + d / 2)),
// Передняя стена левая часть
new THREE.Box3(new THREE.Vector3(ox - w / 2, 0, oz + d / 2 - 0.1), new THREE.Vector3(ox - 1.3, h, oz + d / 2 + 0.1)),
new THREE.Box3(new THREE.Vector3(ox + 1.3, 0, oz + d / 2 - 0.1), new THREE.Vector3(ox + w / 2, h, oz + d / 2 + 0.1)),
// Прилавок коллайдер
new THREE.Box3(new THREE.Vector3(ox - 2.5, 0, oz - 1.8), new THREE.Vector3(ox + 2.5, 1, oz - 1.2)),
);
// Интерактивные объекты внутри
this.interiorInteractables.push(
{
position: new THREE.Vector3(ox, 0, oz - 1.5),
radius: 2.5,
type: 'shop_counter',
label: 'Купить / Продать',
building: 'shop'
},
{
position: new THREE.Vector3(ox, 0, oz + d / 2 - 0.5),
radius: 2,
type: 'exit_door',
label: 'Выйти на улицу',
building: 'shop'
}
);
}
buildHospital() {
const ox = 500, oz = 50;
const w = 12, d = 10, h = 3.5;
const group = new THREE.Group();
// Пол
const floorMat = new THREE.MeshStandardMaterial({ color: 0xe8e8e8 });
const floor = new THREE.Mesh(new THREE.PlaneGeometry(w, d), floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(ox, 0.01, oz);
floor.receiveShadow = true;
group.add(floor);
// Потолок
const ceilMat = new THREE.MeshStandardMaterial({ color: 0xffffff, side: THREE.BackSide });
const ceil = new THREE.Mesh(new THREE.PlaneGeometry(w, d), ceilMat);
ceil.rotation.x = -Math.PI / 2;
ceil.position.set(ox, h, oz);
group.add(ceil);
// Стены (белые)
const wallMat = new THREE.MeshStandardMaterial({ color: 0xf0f0f0 });
const backWall = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.2), wallMat);
backWall.position.set(ox, h / 2, oz - d / 2);
group.add(backWall);
const leftWall = new THREE.Mesh(new THREE.BoxGeometry(0.2, h, d), wallMat);
leftWall.position.set(ox - w / 2, h / 2, oz);
group.add(leftWall);
const rightWall = new THREE.Mesh(new THREE.BoxGeometry(0.2, h, d), wallMat);
rightWall.position.set(ox + w / 2, h / 2, oz);
group.add(rightWall);
const frontLeft = new THREE.Mesh(new THREE.BoxGeometry(w / 2 - 1.4, h, 0.2), wallMat);
frontLeft.position.set(ox - w / 4 - 0.7, h / 2, oz + d / 2);
group.add(frontLeft);
const frontRight = new THREE.Mesh(new THREE.BoxGeometry(w / 2 - 1.4, h, 0.2), wallMat);
frontRight.position.set(ox + w / 4 + 0.7, h / 2, oz + d / 2);
group.add(frontRight);
// Кровати
const bedMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
const frameMat = new THREE.MeshStandardMaterial({ color: 0xaabbcc });
for (let i = 0; i < 3; i++) {
const frame = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.4, 0.9), frameMat);
frame.position.set(ox - 4 + i * 3.5, 0.25, oz - 3);
frame.castShadow = true;
group.add(frame);
const mattress = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.1, 0.7), bedMat);
mattress.position.set(ox - 4 + i * 3.5, 0.5, oz - 3);
group.add(mattress);
}
// Стол врача
const desk = new THREE.Mesh(
new THREE.BoxGeometry(2, 0.8, 1),
new THREE.MeshStandardMaterial({ color: 0xccccdd })
);
desk.position.set(ox + 3, 0.4, oz + 2);
desk.castShadow = true;
group.add(desk);
// Врач за столом
this.createInteriorNPC(group, ox + 3, 0, oz + 1.2, 0xeeeeee, false);
// Красный крест на стене
const crossMat = new THREE.MeshStandardMaterial({ color: 0xff3333 });
const crossH = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.2, 0.05), crossMat);
crossH.position.set(ox, 2.5, oz - d / 2 + 0.15);
group.add(crossH);
const crossV = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.2, 0.05), crossMat);
crossV.position.set(ox, 2.5, oz - d / 2 + 0.15);
group.add(crossV);
// Дверная рама и табличка
this.addDoorFrame(group, ox, oz, d);
// Свет
const light = new THREE.PointLight(0xffffff, 1.2, 18);
light.position.set(ox, h - 0.3, oz);
group.add(light);
this.game.scene.add(group);
this.interiorObjects.push(group);
this.interiorColliders.push(
new THREE.Box3(new THREE.Vector3(ox - w / 2 - 0.1, 0, oz - d / 2 - 0.2), new THREE.Vector3(ox + w / 2 + 0.1, h, oz - d / 2)),
new THREE.Box3(new THREE.Vector3(ox - w / 2 - 0.2, 0, oz - d / 2), new THREE.Vector3(ox - w / 2, h, oz + d / 2)),
new THREE.Box3(new THREE.Vector3(ox + w / 2, 0, oz - d / 2), new THREE.Vector3(ox + w / 2 + 0.2, h, oz + d / 2)),
new THREE.Box3(new THREE.Vector3(ox - w / 2, 0, oz + d / 2 - 0.1), new THREE.Vector3(ox - 0.8, h, oz + d / 2 + 0.1)),
new THREE.Box3(new THREE.Vector3(ox + 0.8, 0, oz + d / 2 - 0.1), new THREE.Vector3(ox + w / 2, h, oz + d / 2 + 0.1)),
new THREE.Box3(new THREE.Vector3(ox + 2, 0, oz + 1.5), new THREE.Vector3(ox + 4, 0.8, oz + 2.5)),
);
this.interiorInteractables.push(
{
position: new THREE.Vector3(ox + 3, 0, oz + 2),
radius: 2.5,
type: 'hospital_desk',
label: 'Лечение',
building: 'hospital'
},
{
position: new THREE.Vector3(ox, 0, oz + d / 2 - 0.5),
radius: 2,
type: 'exit_door',
label: 'Выйти на улицу',
building: 'hospital'
}
);
}
buildChurch() {
const ox = 500, oz = 100;
const w = 10, d = 14, h = 5;
const group = new THREE.Group();
// Пол (деревянный)
const floorMat = new THREE.MeshStandardMaterial({ color: 0xb89060 });
const floor = new THREE.Mesh(new THREE.PlaneGeometry(w, d), floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(ox, 0.01, oz);
floor.receiveShadow = true;
group.add(floor);
// Потолок
const ceilMat = new THREE.MeshStandardMaterial({ color: 0xddd8c0, side: THREE.BackSide });
const ceil = new THREE.Mesh(new THREE.PlaneGeometry(w, d), ceilMat);
ceil.rotation.x = -Math.PI / 2;
ceil.position.set(ox, h, oz);
group.add(ceil);
// Стены
const wallMat = new THREE.MeshStandardMaterial({ color: 0xf0e8d0 });
const backWall = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.2), wallMat);
backWall.position.set(ox, h / 2, oz - d / 2);
group.add(backWall);
const leftWall = new THREE.Mesh(new THREE.BoxGeometry(0.2, h, d), wallMat);
leftWall.position.set(ox - w / 2, h / 2, oz);
group.add(leftWall);
const rightWall = new THREE.Mesh(new THREE.BoxGeometry(0.2, h, d), wallMat);
rightWall.position.set(ox + w / 2, h / 2, oz);
group.add(rightWall);
const frontLeft = new THREE.Mesh(new THREE.BoxGeometry(w / 2 - 1.4, h, 0.2), wallMat);
frontLeft.position.set(ox - w / 4 - 0.7, h / 2, oz + d / 2);
group.add(frontLeft);
const frontRight = new THREE.Mesh(new THREE.BoxGeometry(w / 2 - 1.4, h, 0.2), wallMat);
frontRight.position.set(ox + w / 4 + 0.7, h / 2, oz + d / 2);
group.add(frontRight);
// Скамьи (ряды)
const benchMat = new THREE.MeshStandardMaterial({ color: 0x8b6914 });
for (let row = 0; row < 4; row++) {
for (let side = -1; side <= 1; side += 2) {
const bench = new THREE.Mesh(new THREE.BoxGeometry(3, 0.4, 0.5), benchMat);
bench.position.set(ox + side * 2, 0.25, oz - 3 + row * 2.5);
bench.castShadow = true;
group.add(bench);
}
}
// Алтарь
const altarMat = new THREE.MeshStandardMaterial({ color: 0xf5e6cc });
const altar = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 0.8), altarMat);
altar.position.set(ox, 0.5, oz - d / 2 + 1.5);
altar.castShadow = true;
group.add(altar);
// Крест на стене
const crossMat = new THREE.MeshStandardMaterial({ color: 0xdaa520 });
const crossH = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.15, 0.05), crossMat);
crossH.position.set(ox, 3.5, oz - d / 2 + 0.15);
group.add(crossH);
const crossV = new THREE.Mesh(new THREE.BoxGeometry(0.15, 2, 0.05), crossMat);
crossV.position.set(ox, 3.5, oz - d / 2 + 0.15);
group.add(crossV);
// Свечи
const candleMat = new THREE.MeshStandardMaterial({ color: 0xfff8dc });
for (let i = -1; i <= 1; i++) {
const candle = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.2, 6), candleMat);
candle.position.set(ox + i * 0.4, 1.15, oz - d / 2 + 1.5);
group.add(candle);
}
// Отец Михаил у алтаря
this.createInteriorNPC(group, ox + 1.5, 0, oz - d / 2 + 2, 0x222244, false);
// Борода
const beard = new THREE.Mesh(
new THREE.BoxGeometry(0.22, 0.2, 0.12),
new THREE.MeshStandardMaterial({ color: 0x555555 })
);
beard.position.set(ox + 1.5, 1.3, oz - d / 2 + 2 + 0.15);
group.add(beard);
// Нагрудный крест
const priestCrossH = new THREE.Mesh(
new THREE.BoxGeometry(0.12, 0.03, 0.03),
new THREE.MeshStandardMaterial({ color: 0xdaa520 })
);
priestCrossH.position.set(ox + 1.5, 1.0, oz - d / 2 + 2 + 0.3);
group.add(priestCrossH);
const priestCrossV = new THREE.Mesh(
new THREE.BoxGeometry(0.03, 0.18, 0.03),
new THREE.MeshStandardMaterial({ color: 0xdaa520 })
);
priestCrossV.position.set(ox + 1.5, 1.0, oz - d / 2 + 2 + 0.3);
group.add(priestCrossV);
// Дверная рама и табличка
this.addDoorFrame(group, ox, oz, d);
// Тёплый свет
const light = new THREE.PointLight(0xffe0a0, 0.8, 20);
light.position.set(ox, h - 0.5, oz);
group.add(light);
const altarLight = new THREE.PointLight(0xffcc66, 0.5, 8);
altarLight.position.set(ox, 1.5, oz - d / 2 + 1.5);
group.add(altarLight);
this.game.scene.add(group);
this.interiorObjects.push(group);
this.interiorColliders.push(
new THREE.Box3(new THREE.Vector3(ox - w / 2 - 0.1, 0, oz - d / 2 - 0.2), new THREE.Vector3(ox + w / 2 + 0.1, h, oz - d / 2)),
new THREE.Box3(new THREE.Vector3(ox - w / 2 - 0.2, 0, oz - d / 2), new THREE.Vector3(ox - w / 2, h, oz + d / 2)),
new THREE.Box3(new THREE.Vector3(ox + w / 2, 0, oz - d / 2), new THREE.Vector3(ox + w / 2 + 0.2, h, oz + d / 2)),
new THREE.Box3(new THREE.Vector3(ox - w / 2, 0, oz + d / 2 - 0.1), new THREE.Vector3(ox - 0.8, h, oz + d / 2 + 0.1)),
new THREE.Box3(new THREE.Vector3(ox + 0.8, 0, oz + d / 2 - 0.1), new THREE.Vector3(ox + w / 2, h, oz + d / 2 + 0.1)),
new THREE.Box3(new THREE.Vector3(ox - 1, 0, oz - d / 2 + 1.1), new THREE.Vector3(ox + 1, 1, oz - d / 2 + 1.9)),
);
this.interiorInteractables.push(
{
position: new THREE.Vector3(ox, 0, oz - d / 2 + 1.5),
radius: 2.5,
type: 'church_altar',
label: 'Помолиться / Еда',
building: 'church'
},
{
position: new THREE.Vector3(ox, 0, oz + d / 2 - 0.5),
radius: 2,
type: 'exit_door',
label: 'Выйти на улицу',
building: 'church'
}
);
}
enterBuilding(type) {
if (this.isInside) return;
if (!this.built) this.buildInteriors();
this.savedPosition = this.game.player.position.clone();
this.currentBuilding = type;
this.isInside = true;
// Позиция входа в интерьер
const entries = {
shop: new THREE.Vector3(500, 0, 3),
hospital: new THREE.Vector3(500, 0, 53),
church: new THREE.Vector3(500, 0, 106),
};
const entry = entries[type];
if (entry) {
this.game.player.position.copy(entry);
this.game.player.mesh.position.copy(entry);
}
// Зафиксировать погоду/освещение
this.game.scene.fog = null;
// Подключить интерьерные коллайдеры и интерактивные объекты
this._origColliders = this.game.world.colliders;
this._origInteractables = this.game.world.interactables;
this.game.world.colliders = this.interiorColliders;
this.game.world.interactables = this.interiorInteractables.filter(i => i.building === type);
// Скрыть наружные объекты для производительности
this.game.player.position.x = THREE.MathUtils.clamp(this.game.player.position.x, 490, 510);
this.game.player.position.z = THREE.MathUtils.clamp(this.game.player.position.z, -10, 120);
const names = { shop: 'Магазин', hospital: 'Больница', church: 'Церковь' };
this.game.notify(`Вы вошли: ${names[type] || type}`);
}
exitBuilding() {
if (!this.isInside) return;
this.isInside = false;
// Восстановить позицию
if (this.savedPosition) {
this.game.player.position.copy(this.savedPosition);
this.game.player.mesh.position.copy(this.savedPosition);
}
// Восстановить туман
this.game.scene.fog = new THREE.Fog(0x87CEEB, 80, 200);
// Восстановить коллайдеры
if (this._origColliders) this.game.world.colliders = this._origColliders;
if (this._origInteractables) this.game.world.interactables = this._origInteractables;
this.currentBuilding = null;
this.game.notify('Вы вышли на улицу.');
}
reset() {
if (this.isInside) {
this.exitBuilding();
}
this.interiorObjects.forEach(obj => this.game.scene.remove(obj));
this.interiorObjects = [];
this.interiorColliders = [];
this.interiorInteractables = [];
this.built = false;
}
}