325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
import * as THREE from 'three';
|
||
|
||
export class Dangers {
|
||
constructor(game) {
|
||
this.game = game;
|
||
this.enemies = [];
|
||
this.spawnTimer = 60 + Math.random() * 60;
|
||
this.maxEnemies = 2;
|
||
}
|
||
|
||
update(dt) {
|
||
this.spawnTimer -= dt;
|
||
|
||
if (this.spawnTimer <= 0 && this.enemies.length < this.maxEnemies) {
|
||
this.trySpawn();
|
||
this.spawnTimer = 80 + Math.random() * 120;
|
||
}
|
||
|
||
this.updateEnemies(dt);
|
||
}
|
||
|
||
trySpawn() {
|
||
const hour = this.game.gameTime / 60;
|
||
const isNight = hour < 6 || hour > 21;
|
||
const baseChance = isNight ? 0.7 : 0.15;
|
||
const dangerMod = this.game.reputation.getDangerModifier();
|
||
|
||
if (Math.random() > baseChance * dangerMod) return;
|
||
|
||
// Проверяем безопасную зону (укрытие с дверью)
|
||
if (this.game.housing.isSafeZone(this.game.player.position)) return;
|
||
|
||
this.spawnEnemy();
|
||
}
|
||
|
||
spawnEnemy() {
|
||
const player = this.game.player;
|
||
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const dist = 30 + Math.random() * 20;
|
||
const x = THREE.MathUtils.clamp(player.position.x + Math.cos(angle) * dist, -90, 90);
|
||
const z = THREE.MathUtils.clamp(player.position.z + Math.sin(angle) * dist, -90, 90);
|
||
|
||
const types = [
|
||
{ type: 'thug', name: 'Хулиган', color: 0x992222, speed: 4.2, damage: 15, moneySteal: 0.2, hp: 3 },
|
||
{ type: 'thief', name: 'Вор', color: 0x444466, speed: 5.2, damage: 5, moneySteal: 0.5, hp: 2 },
|
||
{ type: 'drunk', name: 'Пьяница', color: 0x886633, speed: 3.0, damage: 10, moneySteal: 0.1, hp: 2 },
|
||
{ type: 'gang', name: 'Гопник', color: 0x993366, speed: 4.5, damage: 20, moneySteal: 0.3, hp: 4 },
|
||
];
|
||
|
||
// Гопники только ночью
|
||
const available = this.game.isNight() ? types : types.filter(t => t.type !== 'gang');
|
||
const template = available[Math.floor(Math.random() * available.length)];
|
||
|
||
const enemy = {
|
||
...template,
|
||
position: new THREE.Vector3(x, 0, z),
|
||
mesh: null,
|
||
state: 'approach',
|
||
lifetime: 60,
|
||
attackCooldown: 0,
|
||
detectionRange: 25,
|
||
attackRange: 2,
|
||
stunTimer: 0,
|
||
maxHp: template.hp,
|
||
};
|
||
|
||
// Создаём меш
|
||
const group = new THREE.Group();
|
||
const bodyMat = new THREE.MeshStandardMaterial({ color: template.color });
|
||
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.37, 1.15, 8), bodyMat);
|
||
body.position.y = 0.85;
|
||
body.castShadow = true;
|
||
group.add(body);
|
||
|
||
const head = new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.23, 8, 6),
|
||
new THREE.MeshStandardMaterial({ color: 0xc49070 })
|
||
);
|
||
head.position.y = 1.57;
|
||
group.add(head);
|
||
|
||
if (template.type === 'thug' || template.type === 'gang') {
|
||
const hood = new THREE.Mesh(
|
||
new THREE.CylinderGeometry(0.24, 0.27, 0.18, 8),
|
||
new THREE.MeshStandardMaterial({ color: template.type === 'gang' ? 0x333333 : 0x222222 })
|
||
);
|
||
hood.position.y = 1.72;
|
||
group.add(hood);
|
||
} else if (template.type === 'thief') {
|
||
const mask = new THREE.Mesh(
|
||
new THREE.BoxGeometry(0.2, 0.1, 0.25),
|
||
new THREE.MeshStandardMaterial({ color: 0x111111 })
|
||
);
|
||
mask.position.set(0, 1.47, 0.1);
|
||
group.add(mask);
|
||
} else {
|
||
// Пьяница — красный нос
|
||
const nose = new THREE.Mesh(
|
||
new THREE.SphereGeometry(0.06, 6, 4),
|
||
new THREE.MeshStandardMaterial({ color: 0xcc3333 })
|
||
);
|
||
nose.position.set(0, 1.5, 0.2);
|
||
group.add(nose);
|
||
}
|
||
|
||
// Полоска здоровья
|
||
const hpBarBg = new THREE.Mesh(
|
||
new THREE.PlaneGeometry(0.8, 0.08),
|
||
new THREE.MeshBasicMaterial({ color: 0x333333, side: THREE.DoubleSide })
|
||
);
|
||
hpBarBg.position.y = 2.0;
|
||
group.add(hpBarBg);
|
||
|
||
const hpBarFill = new THREE.Mesh(
|
||
new THREE.PlaneGeometry(0.78, 0.06),
|
||
new THREE.MeshBasicMaterial({ color: 0xcc2222, side: THREE.DoubleSide })
|
||
);
|
||
hpBarFill.position.y = 2.0;
|
||
hpBarFill.position.z = 0.001;
|
||
group.add(hpBarFill);
|
||
enemy._hpBar = hpBarFill;
|
||
|
||
group.position.copy(enemy.position);
|
||
this.game.scene.add(group);
|
||
enemy.mesh = group;
|
||
|
||
this.enemies.push(enemy);
|
||
this.game.notify(`${enemy.name} замечен поблизости!`, 'bad');
|
||
this.game.sound.playHurt();
|
||
}
|
||
|
||
updateEnemies(dt) {
|
||
for (let i = this.enemies.length - 1; i >= 0; i--) {
|
||
const enemy = this.enemies[i];
|
||
enemy.lifetime -= dt;
|
||
|
||
if (enemy.lifetime <= 0) {
|
||
this.removeEnemy(i);
|
||
continue;
|
||
}
|
||
|
||
if (enemy.stunTimer > 0) {
|
||
enemy.stunTimer -= dt;
|
||
continue;
|
||
}
|
||
|
||
const player = this.game.player;
|
||
const dir = new THREE.Vector3().subVectors(player.position, enemy.position);
|
||
dir.y = 0;
|
||
const dist = dir.length();
|
||
|
||
// Проверка безопасной зоны
|
||
if (this.game.housing.isSafeZone(player.position)) {
|
||
enemy.state = 'flee';
|
||
}
|
||
|
||
// Полоска здоровья поворот к камере
|
||
if (enemy._hpBar) {
|
||
const scale = enemy.hp / enemy.maxHp;
|
||
enemy._hpBar.scale.x = Math.max(0.01, scale);
|
||
enemy._hpBar.material.color.setHex(scale > 0.5 ? 0xcc2222 : 0xff4444);
|
||
enemy.mesh.children.forEach(child => {
|
||
if (child === enemy._hpBar || child === enemy._hpBar) {
|
||
child.lookAt(this.game.camera.position);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Убегает
|
||
if (enemy.state === 'flee') {
|
||
const fleeDir = dir.clone().normalize().negate();
|
||
enemy.position.add(fleeDir.multiplyScalar(enemy.speed * dt));
|
||
enemy.mesh.position.copy(enemy.position);
|
||
enemy.mesh.rotation.y = Math.atan2(-dir.x, -dir.z);
|
||
|
||
if (dist > 50) {
|
||
this.removeEnemy(i);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Обнаружение
|
||
if (dist < enemy.detectionRange) {
|
||
enemy.state = 'chase';
|
||
}
|
||
|
||
if (enemy.state === 'chase') {
|
||
if (dist > enemy.attackRange) {
|
||
dir.normalize();
|
||
enemy.position.add(dir.multiplyScalar(enemy.speed * dt));
|
||
enemy.mesh.position.copy(enemy.position);
|
||
enemy.mesh.rotation.y = Math.atan2(dir.x, dir.z);
|
||
} else {
|
||
if (enemy.attackCooldown <= 0) {
|
||
this.attackPlayer(enemy);
|
||
enemy.attackCooldown = 3;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (enemy.attackCooldown > 0) {
|
||
enemy.attackCooldown -= dt;
|
||
}
|
||
|
||
enemy.position.x = THREE.MathUtils.clamp(enemy.position.x, -95, 95);
|
||
enemy.position.z = THREE.MathUtils.clamp(enemy.position.z, -95, 95);
|
||
}
|
||
}
|
||
|
||
attackPlayer(enemy) {
|
||
const player = this.game.player;
|
||
|
||
// Защита от экипировки
|
||
const protection = this.game.equipment.getProtectionBonus();
|
||
const damage = Math.max(1, Math.floor(enemy.damage * (1 - protection / 100)));
|
||
|
||
player.stats.health = Math.max(0, player.stats.health - damage);
|
||
this.game.sound.playHurt();
|
||
|
||
if (enemy.type === 'thief') {
|
||
const stolen = Math.floor(player.stats.money * enemy.moneySteal);
|
||
if (stolen > 0) {
|
||
player.stats.money -= stolen;
|
||
this.game.notify(`${enemy.name} украл у вас ${stolen}₽!`, 'bad');
|
||
} else {
|
||
this.game.notify(`${enemy.name} толкнул вас! -${damage} Здоровье`, 'bad');
|
||
}
|
||
enemy.state = 'flee';
|
||
} else {
|
||
this.game.notify(`${enemy.name} ударил вас! -${damage} Здоровье`, 'bad');
|
||
}
|
||
|
||
player.stats.mood = Math.max(0, player.stats.mood - 10);
|
||
}
|
||
|
||
playerFightBack() {
|
||
const player = this.game.player;
|
||
let hitEnemy = null;
|
||
let minDist = 4;
|
||
|
||
for (const enemy of this.enemies) {
|
||
const dist = player.position.distanceTo(enemy.position);
|
||
if (dist < minDist) {
|
||
minDist = dist;
|
||
hitEnemy = enemy;
|
||
}
|
||
}
|
||
|
||
if (!hitEnemy) return false;
|
||
|
||
const combatLevel = this.game.skills.getLevel('survival');
|
||
let hitChance = 0.4 + combatLevel * 0.06;
|
||
|
||
// Бонус от оружия
|
||
if (this.game.inventory.getCount('eq_pipe') > 0) {
|
||
hitChance += 0.25;
|
||
} else if (this.game.inventory.getCount('eq_stick') > 0) {
|
||
hitChance += 0.15;
|
||
}
|
||
|
||
if (Math.random() < hitChance) {
|
||
hitEnemy.stunTimer = 2;
|
||
hitEnemy.hp--;
|
||
this.game.particles.createSparks(hitEnemy.position.clone().add(new THREE.Vector3(0, 1, 0)));
|
||
|
||
if (hitEnemy.hp <= 0) {
|
||
this.game.notify(`${hitEnemy.name} повержен!`, 'good');
|
||
this.game.reputation.change(3);
|
||
this.game.enemiesDefeated++;
|
||
this.game.consecutiveFights++;
|
||
this.game.achievements.check('first_fight');
|
||
if (this.game.enemiesDefeated >= 5) {
|
||
this.game.achievements.check('fighter_5');
|
||
}
|
||
if (this.game.consecutiveFights >= 3) {
|
||
this.game.achievements.check('survivor_combat');
|
||
}
|
||
this.game.questSystem.onEvent('defeat_enemy');
|
||
|
||
const idx = this.enemies.indexOf(hitEnemy);
|
||
if (idx >= 0) {
|
||
hitEnemy.state = 'flee';
|
||
hitEnemy.lifetime = 5;
|
||
}
|
||
} else {
|
||
this.game.notify('Вы дали отпор!', 'good');
|
||
if (Math.random() < 0.3) {
|
||
hitEnemy.state = 'flee';
|
||
this.game.notify(`${hitEnemy.name} убегает!`, 'good');
|
||
}
|
||
}
|
||
|
||
this.game.skills.addXP('survival', 2);
|
||
} else {
|
||
this.game.notify('Промах!', 'bad');
|
||
this.game.consecutiveFights = 0;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
hasNearbyDanger() {
|
||
const player = this.game.player;
|
||
for (const enemy of this.enemies) {
|
||
if (player.position.distanceTo(enemy.position) < 15) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
removeEnemy(index) {
|
||
const enemy = this.enemies[index];
|
||
if (enemy.mesh) {
|
||
this.game.scene.remove(enemy.mesh);
|
||
}
|
||
this.enemies.splice(index, 1);
|
||
}
|
||
|
||
reset() {
|
||
for (let i = this.enemies.length - 1; i >= 0; i--) {
|
||
this.removeEnemy(i);
|
||
}
|
||
this.spawnTimer = 60 + Math.random() * 60;
|
||
}
|
||
}
|