Files
Hommie_RPG_Game/js/game/Dangers.js
Maxim Dolgolyov fb5f09212b Initial commit: 3D Hommie RPG game
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:04:09 +03:00

325 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}