Files
firstProj/src/controllers/Map/PropC.ts
T
2026-06-02 18:22:20 +03:00

244 lines
6.7 KiB
TypeScript

import { EasyEvent, UpdateController } from "@24tools/playable_template";
import { Euler, Material, Mesh, Object3D, Vector3 } from "three";
import { PhysicsBody } from "../PhysicsC";
import { PropType } from "./PropType";
import { PhysicsTriggerC } from "./PhysicsTriggerC";
import { PROP_DROPS } from "../Resources/PropDropTable";
import { createDropPlan, PropDropPlan } from "../Resources/PropDropPlanner";
import { ResourceSpawnC } from "../Resources/ResourceSpawnC";
import { PropHpUIC } from "./PropHpUIC";
import { PropHpBar } from "./PropHpBar";
import { PropVfxC } from "./PropVfxC";
import { VFXType } from "../Enums/VFXType";
/** Тривалість тряски після удару (секунди). */
const SHAKE_DURATION = 0.22;
/** Амплітуда зміщення моделі по X/Z під час тряски (world units). */
const SHAKE_POSITION_AMP = 0.045;
/** Амплітуда нахилу моделі під час тряски (радіани). */
const SHAKE_ROTATION_AMP = 0.1;
/** Частота коливань тряски (кількість «хитань» за секунду). */
const SHAKE_FREQUENCY = 2;
export class PropRegistry {
private static props = new Map<Object3D, PropC>();
static register(prop: PropC) {
this.props.set(prop.object, prop);
}
static get(object: Object3D) {
return this.props.get(object);
}
static unregister(object: Object3D) {
this.props.delete(object);
}
}
export class PropC {
static readonly onBroken = new EasyEvent<PropC>();
readonly object: Object3D;
readonly propType: PropType;
health: number;
readonly maxHealth: number;
isBroken = false;
private wallBodies: PhysicsBody[] = [];
private dropPlan: PropDropPlan | null = null;
private hpBar: PropHpBar | null = null;
private damageLayers: Object3D[] = [];
private readonly restPosition = new Vector3();
private readonly restRotation = new Euler();
private shakeTimeLeft = 0;
private shakeElapsed = 0;
private onShakeComplete: (() => void) | null = null;
private static shakingProps = new Set<PropC>();
private static shakeSystemInited = false;
constructor(object: Object3D, propType: PropType, maxHealth = 1) {
this.object = object;
this.propType = propType;
this.maxHealth = maxHealth;
this.health = maxHealth;
this.restPosition.copy(object.position);
this.restRotation.copy(object.rotation);
PropRegistry.register(this);
this.hpBar = PropHpUIC.createBar(this) ?? null;
PropC.ensureShakeSystem();
}
private static ensureShakeSystem() {
if (this.shakeSystemInited) return;
this.shakeSystemInited = true;
UpdateController.Instance.onUpdate.addDelegate((delta) => {
for (const prop of [...this.shakingProps]) {
prop.updateShake(delta);
}
});
}
addWallBody(physicsBody: PhysicsBody) {
this.wallBodies.push(physicsBody);
}
initDamageLayers(layers: Object3D[]) {
this.damageLayers = layers;
}
takeDamage(amount: number): boolean {
if (this.isBroken) return false;
const hitIndex = this.maxHealth - this.health;
this.ensureDropPlan();
this.applyDamageVisual(hitIndex);
this.health -= amount;
this.spawnForHit(hitIndex);
this.hpBar?.onDamage();
PropVfxC.Play(VFXType.LootableHit, null, this.object.getWorldPosition(new Vector3()));
if (this.health > 0) {
this.playHitShake();
return false;
}
this.playHitShake(() => this.break());
return true;
}
private applyDamageVisual(hitIndex: number) {
const layer = this.damageLayers[hitIndex];
if (!layer) return;
layer.visible = false;
}
private playHitShake(onComplete?: () => void) {
if (this.isBroken) return;
if (onComplete) {
this.onShakeComplete = onComplete;
}
this.shakeTimeLeft = SHAKE_DURATION;
this.shakeElapsed = 0;
PropC.shakingProps.add(this);
}
private updateShake(delta: number) {
if (this.shakeTimeLeft <= 0) {
this.finishShake();
return;
}
this.shakeTimeLeft -= delta;
this.shakeElapsed += delta;
const progress = Math.max(this.shakeTimeLeft / SHAKE_DURATION, 0);
const intensity = progress * progress;
const phase = this.shakeElapsed * SHAKE_FREQUENCY * Math.PI * 2;
const wobble = Math.sin(phase);
this.object.position.set(
this.restPosition.x + wobble * SHAKE_POSITION_AMP * intensity,
this.restPosition.y,
this.restPosition.z + Math.cos(phase) * SHAKE_POSITION_AMP * intensity * 0.75,
);
this.object.rotation.set(
this.restRotation.x + wobble * SHAKE_ROTATION_AMP * intensity * 0.35,
this.restRotation.y,
this.restRotation.z + Math.cos(phase) * SHAKE_ROTATION_AMP * intensity,
);
if (this.shakeTimeLeft <= 0) {
this.finishShake();
}
}
private finishShake() {
this.stopShake();
const callback = this.onShakeComplete;
this.onShakeComplete = null;
callback?.();
}
private stopShake() {
this.object.position.copy(this.restPosition);
this.object.rotation.copy(this.restRotation);
this.shakeTimeLeft = 0;
PropC.shakingProps.delete(this);
}
break() {
if (this.isBroken) return;
this.isBroken = true;
this.onShakeComplete = null;
this.stopShake();
PropHpUIC.removeBar(this);
this.hpBar = null;
this.wallBodies.forEach((body) => body.destroy());
this.wallBodies = [];
PhysicsTriggerC.unregister(this.object);
PropC.onBroken.Invoke(this);
PropRegistry.unregister(this.object);
PropVfxC.Play(VFXType.LootableDestroy, null, this.object.getWorldPosition(new Vector3()));
this.disposeProceduralResources();
this.object.parent?.remove(this.object);
}
private disposeProceduralResources() {
const trigger = this.object.getObjectByName("GatherTrigger");
if (!trigger || !(trigger as Mesh).isMesh) return;
const mesh = trigger as Mesh;
mesh.geometry.dispose();
const material = mesh.material;
if (Array.isArray(material)) {
material.forEach((entry) => entry.dispose());
} else if (material) {
(material as Material).dispose();
}
}
private ensureDropPlan() {
if (this.dropPlan) return;
const rules = PROP_DROPS[this.propType];
if (!rules.length) return;
this.dropPlan = createDropPlan(this.maxHealth, rules[0]);
}
private spawnForHit(hitIndex: number) {
if (!this.dropPlan) return;
let spawnCount = this.dropPlan.hitAmounts[hitIndex] ?? 0;
if (this.health <= 0) {
const remaining = this.dropPlan.totalDrop - this.dropPlan.spawnedDrop - spawnCount;
if (remaining > 0) {
spawnCount += remaining;
}
}
if (spawnCount <= 0) return;
ResourceSpawnC.spawnFromProp(this, this.dropPlan.resource, spawnCount);
this.dropPlan.spawnedDrop += spawnCount;
}
}