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

324
js/game/Dangers.js Normal file
View File

@@ -0,0 +1,324 @@
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;
}
}