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