195 lines
6.1 KiB
JavaScript
195 lines
6.1 KiB
JavaScript
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
|
||
);
|
||
}
|
||
}
|