275 lines
9.2 KiB
JavaScript
275 lines
9.2 KiB
JavaScript
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;
|
||
}
|
||
}
|