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

194
js/game/Dog.js Normal file
View File

@@ -0,0 +1,194 @@
import * as THREE from 'three';
export class Dog {
constructor(game) {
this.game = game;
this.mesh = null;
this.position = new THREE.Vector3(-25, 0, 30); // default, обновляется из конфига в spawn()
this.adopted = false;
this.followDistance = 3;
this.speed = 4;
this.moodTimer = 0;
this.tailWag = 0;
this.tail = null;
}
spawn() {
// Позиция из конфига (рядом с парком)
const parkCfg = this.game.world.mapConfig?.structures?.park || {};
this.position.set(
(parkCfg.x ?? -30) + 5,
0,
(parkCfg.z ?? 25) + 5
);
const group = new THREE.Group();
// Тело
const bodyGeo = new THREE.BoxGeometry(0.8, 0.45, 0.4);
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x8B6914 });
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.y = 0.45;
body.castShadow = true;
group.add(body);
// Голова
const headGeo = new THREE.BoxGeometry(0.3, 0.3, 0.35);
const head = new THREE.Mesh(headGeo, bodyMat);
head.position.set(0.45, 0.6, 0);
head.castShadow = true;
group.add(head);
// Морда
const noseGeo = new THREE.BoxGeometry(0.12, 0.12, 0.2);
const noseMat = new THREE.MeshStandardMaterial({ color: 0x4a3010 });
const nose = new THREE.Mesh(noseGeo, noseMat);
nose.position.set(0.6, 0.55, 0);
group.add(nose);
// Нос
const noseTip = new THREE.Mesh(
new THREE.SphereGeometry(0.04, 6, 4),
new THREE.MeshStandardMaterial({ color: 0x222222 })
);
noseTip.position.set(0.67, 0.56, 0);
group.add(noseTip);
// Уши
[-0.14, 0.14].forEach(side => {
const earGeo = new THREE.BoxGeometry(0.08, 0.15, 0.06);
const ear = new THREE.Mesh(earGeo, bodyMat);
ear.position.set(0.4, 0.8, side);
ear.rotation.z = side > 0 ? 0.3 : -0.3;
group.add(ear);
});
// Глаза
const eyeMat = new THREE.MeshStandardMaterial({ color: 0x222222 });
[-0.1, 0.1].forEach(side => {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.03, 6, 4), eyeMat);
eye.position.set(0.55, 0.65, side);
group.add(eye);
});
// Ноги
const legGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.3, 6);
const legMat = new THREE.MeshStandardMaterial({ color: 0x7a5a10 });
[[-0.25, -0.14], [-0.25, 0.14], [0.25, -0.14], [0.25, 0.14]].forEach(([lx, lz]) => {
const leg = new THREE.Mesh(legGeo, legMat);
leg.position.set(lx, 0.15, lz);
group.add(leg);
});
// Хвост
const tailGeo = new THREE.CylinderGeometry(0.03, 0.02, 0.3, 6);
const tail = new THREE.Mesh(tailGeo, bodyMat);
tail.position.set(-0.5, 0.6, 0);
tail.rotation.z = Math.PI / 4;
group.add(tail);
this.tail = tail;
group.position.copy(this.position);
this.mesh = group;
this.game.scene.add(group);
}
adopt() {
this.adopted = true;
this.game.player.stats.mood = Math.min(100, this.game.player.stats.mood + 20);
}
update(dt) {
if (!this.mesh) return;
// Виляние хвостом
this.tailWag += dt * 8;
if (this.tail) {
this.tail.rotation.x = Math.sin(this.tailWag) * 0.4;
}
if (this.adopted) {
this.followPlayer(dt);
this.applyMoodBoost(dt);
} else {
this.wander(dt);
}
this.mesh.position.copy(this.position);
}
followPlayer(dt) {
const playerPos = this.game.player.position;
const dir = new THREE.Vector3().subVectors(playerPos, this.position);
dir.y = 0;
const dist = dir.length();
if (dist > this.followDistance) {
dir.normalize();
const speed = dist > 8 ? this.speed * 2 : this.speed;
this.position.add(dir.multiplyScalar(speed * dt));
// Поворот к игроку
this.mesh.rotation.y = Math.atan2(dir.x, dir.z);
}
// Телепортация если слишком далеко
if (dist > 25) {
const behind = new THREE.Vector3();
this.game.camera.getWorldDirection(behind);
behind.y = 0;
behind.normalize().multiplyScalar(-3);
this.position.copy(playerPos).add(behind);
}
}
wander(dt) {
// Бродит рядом с парком
if (!this._wanderTarget || this.position.distanceTo(this._wanderTarget) < 1) {
const parkCfg = this.game.world.mapConfig?.structures?.park || {};
const cx = parkCfg.x ?? -30;
const cz = parkCfg.z ?? 25;
this._wanderTarget = new THREE.Vector3(
cx + (Math.random() - 0.5) * 20,
0,
cz + (Math.random() - 0.5) * 16
);
this._wanderWait = 2 + Math.random() * 3;
}
if (this._wanderWait > 0) {
this._wanderWait -= dt;
return;
}
const dir = new THREE.Vector3().subVectors(this._wanderTarget, this.position);
dir.y = 0;
if (dir.length() > 0.5) {
dir.normalize();
this.position.add(dir.multiplyScalar(1.5 * dt));
this.mesh.rotation.y = Math.atan2(dir.x, dir.z);
}
}
applyMoodBoost(dt) {
this.moodTimer += dt;
if (this.moodTimer >= 10) {
this.moodTimer = 0;
this.game.player.stats.mood = Math.min(100, this.game.player.stats.mood + 1);
}
}
reset() {
if (this.mesh) {
this.game.scene.remove(this.mesh);
this.mesh = null;
}
this.adopted = false;
const parkCfg = this.game.world.mapConfig?.structures?.park || {};
this.position.set(
(parkCfg.x ?? -30) + 5,
0,
(parkCfg.z ?? 25) + 5
);
}
}