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; } }