244 lines
6.7 KiB
TypeScript
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;
|
|
}
|
|
}
|