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

274
js/game/Police.js Normal file
View File

@@ -0,0 +1,274 @@
import * as THREE from 'three';
export class Police {
constructor(game) {
this.game = game;
this.officers = [];
this.warningCooldown = 0;
}
spawnPatrols() {
this.officers = [];
// Два патруля по дорогам
const routes = [
// Патруль 1: вдоль главной E-W дороги (тротуар z=9)
[
{ x: -50, z: 9 },
{ x: -10, z: 9 },
{ x: 25, z: 9 },
{ x: 50, z: 9 },
{ x: 25, z: 9 },
{ x: -10, z: 9 },
],
// Патруль 2: вдоль N-S (тротуар x=9) и южной
[
{ x: 9, z: -35 },
{ x: 9, z: -5 },
{ x: 9, z: 15 },
{ x: 9, z: 40 },
{ x: 9, z: 15 },
{ x: 9, z: -5 },
],
];
routes.forEach((waypoints, i) => {
const officer = {
position: new THREE.Vector3(waypoints[0].x, 0, waypoints[0].z),
waypoints,
currentWP: 0,
state: 'patrol', // patrol, warning, chase, fine
speed: 3,
chaseSpeed: 5.5,
detectRange: 20,
warnTimer: 0,
mesh: null,
warningGiven: false,
};
officer.mesh = this.createOfficerMesh();
officer.mesh.position.copy(officer.position);
this.game.scene.add(officer.mesh);
this.officers.push(officer);
});
}
createOfficerMesh() {
const group = new THREE.Group();
// Тело (синяя форма)
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x1a3366 });
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);
// Фуражка
const capBrim = new THREE.Mesh(
new THREE.CylinderGeometry(0.28, 0.28, 0.04, 8),
new THREE.MeshStandardMaterial({ color: 0x1a2244 })
);
capBrim.position.y = 1.72;
group.add(capBrim);
const capTop = new THREE.Mesh(
new THREE.CylinderGeometry(0.18, 0.22, 0.14, 8),
new THREE.MeshStandardMaterial({ color: 0x1a2244 })
);
capTop.position.y = 1.80;
group.add(capTop);
// Ноги
const legMat = new THREE.MeshStandardMaterial({ color: 0x1a2244 });
[-0.12, 0.12].forEach(side => {
const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 0.5, 6), legMat);
leg.position.set(side, 0.25, 0);
leg.castShadow = true;
group.add(leg);
});
return group;
}
update(dt) {
if (this.warningCooldown > 0) this.warningCooldown -= dt;
// Ночью полиция не патрулирует
const hour = this.game.gameTime / 60;
const isNightTime = hour < 6 || hour > 22;
this.officers.forEach(officer => {
if (isNightTime) {
// Уходят за карту ночью
if (officer.mesh) officer.mesh.visible = false;
officer.state = 'patrol';
return;
}
if (officer.mesh) officer.mesh.visible = true;
switch (officer.state) {
case 'patrol':
this.updatePatrol(officer, dt);
this.checkViolation(officer);
break;
case 'warning':
this.updateWarning(officer, dt);
break;
case 'chase':
this.updateChase(officer, dt);
break;
case 'fine':
// Dialog in progress, do nothing
break;
}
});
}
updatePatrol(officer, dt) {
const wp = officer.waypoints[officer.currentWP];
const target = new THREE.Vector3(wp.x, 0, wp.z);
const dir = target.clone().sub(officer.position);
const dist = dir.length();
if (dist < 1) {
officer.currentWP = (officer.currentWP + 1) % officer.waypoints.length;
} else {
dir.normalize();
officer.position.add(dir.multiplyScalar(officer.speed * dt));
officer.mesh.position.copy(officer.position);
officer.mesh.rotation.y = Math.atan2(dir.x, dir.z);
}
}
checkViolation(officer) {
const playerPos = this.game.player.position;
const dist = officer.position.distanceTo(playerPos);
const detectRange = this.getDetectRange();
if (dist > detectRange) return;
// Попрошайничество в зоне видимости
if (this.game.player.isBegging) {
officer.state = 'warning';
officer.warnTimer = 3;
officer.warningGiven = true;
this.game.notify('Полиция: "Прекратите попрошайничать!"', 'bad');
return;
}
// Очень низкая репутация
if (this.game.reputation.value < -30 && dist < 15) {
officer.state = 'chase';
this.game.notify('Полиция: "Стоять! Проверка документов!"', 'bad');
}
}
updateWarning(officer, dt) {
officer.warnTimer -= dt;
// Смотреть на игрока
const dir = this.game.player.position.clone().sub(officer.position);
officer.mesh.rotation.y = Math.atan2(dir.x, dir.z);
if (officer.warnTimer <= 0) {
// Если всё ещё попрошайничает — переход в chase
if (this.game.player.isBegging) {
officer.state = 'chase';
this.game.notify('Полиция: "Я предупреждал!"', 'bad');
} else {
officer.state = 'patrol';
officer.warningGiven = false;
}
}
}
updateChase(officer, dt) {
const playerPos = this.game.player.position;
const dir = playerPos.clone().sub(officer.position);
const dist = dir.length();
if (dist < 2) {
// Поймал
officer.state = 'fine';
this.showFineDialog(officer);
return;
}
if (dist > 50) {
// Потерял
officer.state = 'patrol';
this.game.notify('Полиция потеряла вас из виду.');
return;
}
dir.normalize();
officer.position.add(dir.multiplyScalar(officer.chaseSpeed * dt));
officer.mesh.position.copy(officer.position);
officer.mesh.rotation.y = Math.atan2(dir.x, dir.z);
}
showFineDialog(officer) {
this.game.player.stopBegging();
this.game.player.stopBusking();
const fineAmount = 50 + Math.floor(Math.random() * 50);
this.game.ui.showDialog('Полицейский', `Нарушение общественного порядка. Штраф ${fineAmount} ₽.`, [
`Заплатить штраф (${fineAmount} ₽)`,
'Нет денег...'
], (index) => {
if (index === 0) {
if (this.game.player.stats.money >= fineAmount) {
this.game.player.stats.money -= fineAmount;
this.game.notify(`Оплачен штраф ${fineAmount} ₽.`, 'bad');
} else {
this.game.notify('Не хватает денег. Конфискация предметов.', 'bad');
this.confiscateItem();
}
} else {
this.game.notify('Конфискация предметов.', 'bad');
this.confiscateItem();
}
this.game.reputation.change(-10);
officer.state = 'patrol';
this.game.ui.hideDialog();
});
}
confiscateItem() {
const inv = this.game.inventory;
const items = Object.keys(inv.items).filter(k => inv.items[k] > 0);
if (items.length > 0) {
const item = items[Math.floor(Math.random() * items.length)];
const name = inv.itemData[item]?.name || item;
inv.removeItem(item, 1);
this.game.notify(`Конфисковано: ${name}`, 'bad');
}
}
getDetectRange() {
const rep = this.game.reputation.value;
if (rep >= 50) return 0; // Уважаемых не трогают
if (rep >= 20) return 10;
if (rep <= -50) return 25;
if (rep <= -20) return 22;
return 20;
}
reset() {
this.officers.forEach(o => {
if (o.mesh) this.game.scene.remove(o.mesh);
});
this.officers = [];
this.warningCooldown = 0;
}
}