154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
import { UpdateController } from "@24tools/playable_template";
|
|
import { Color, Material, Mesh, Object3D, Vector3 } from "three";
|
|
import { ResourceScreenFly } from "../Resources/ResourceScreenFly";
|
|
import {
|
|
findInteractiveZoneFromMap,
|
|
getInteractiveZoneCenter,
|
|
INTERACTIVE_ZONE_NAME,
|
|
} from "./MapInteractiveZone";
|
|
|
|
const FILL_TARGET = 10;
|
|
const FILL_COLOR = new Color(0x2db83a);
|
|
|
|
/** Амплітуда «поглинаючого» імпульсу (частка від базового масштабу). 0.05 = 5%. */
|
|
const PULSE_AMPLITUDE = 0.05;
|
|
/** Тривалість одного імпульсу, сек: базовий розмір → пік → базовий. */
|
|
const PULSE_DURATION = 0.18;
|
|
/** Значення таймера, коли імпульс неактивний. */
|
|
const PULSE_IDLE = Infinity;
|
|
|
|
export class InteractiveZoneC {
|
|
private static zoneRoot: Object3D | null = null;
|
|
private static deposited = 0;
|
|
private static baseColors = new WeakMap<Material, Color>();
|
|
private static baseScale = new Vector3(1, 1, 1);
|
|
private static pulseTime = PULSE_IDLE;
|
|
|
|
static init(mapObject: Object3D) {
|
|
const zone = findInteractiveZoneFromMap(mapObject);
|
|
if (!zone) {
|
|
console.warn(`Interactive zone not found: ${INTERACTIVE_ZONE_NAME}`);
|
|
return;
|
|
}
|
|
|
|
this.zoneRoot = zone;
|
|
this.baseScale.copy(zone.scale);
|
|
this.setupMaterials(zone);
|
|
|
|
UpdateController.Instance.onUpdate.addDelegate((delta: number) => {
|
|
this.updatePulse(delta);
|
|
});
|
|
}
|
|
|
|
/** Один «поглинаючий» імпульс зони — викликати на приліт кожного ресурсу. */
|
|
static pulse() {
|
|
this.pulseTime = 0;
|
|
}
|
|
|
|
/** Повертає зону до базового розміру і гасить імпульс (teardown/зупинка). */
|
|
static resetPulse() {
|
|
this.pulseTime = PULSE_IDLE;
|
|
if (this.zoneRoot) this.zoneRoot.scale.copy(this.baseScale);
|
|
}
|
|
|
|
private static updatePulse(delta: number) {
|
|
if (!this.zoneRoot || this.pulseTime === PULSE_IDLE) return;
|
|
|
|
this.pulseTime += delta;
|
|
if (this.pulseTime >= PULSE_DURATION) {
|
|
this.resetPulse();
|
|
return;
|
|
}
|
|
|
|
const env = Math.sin(Math.PI * (this.pulseTime / PULSE_DURATION));
|
|
const k = 1 + PULSE_AMPLITUDE * env;
|
|
this.zoneRoot.scale.set(
|
|
this.baseScale.x * k,
|
|
this.baseScale.y * k,
|
|
this.baseScale.z * k,
|
|
);
|
|
}
|
|
|
|
static getWorldCenter(out = new Vector3()) {
|
|
if (!this.zoneRoot) return out.set(0, 0, 0);
|
|
return getInteractiveZoneCenter(this.zoneRoot, out);
|
|
}
|
|
|
|
static getScreenCenter() {
|
|
return ResourceScreenFly.worldToScreen(this.getWorldCenter());
|
|
}
|
|
|
|
static addDeposit(amount = 1) {
|
|
this.deposited += amount;
|
|
this.refreshFillVisual();
|
|
}
|
|
|
|
/** Звільняє клоновані матеріали зони (виклик на teardown playable). */
|
|
static dispose() {
|
|
if (!this.zoneRoot) return;
|
|
|
|
this.resetPulse();
|
|
|
|
this.zoneRoot.traverse((child) => {
|
|
const mesh = child as Mesh;
|
|
if (!mesh.isMesh) return;
|
|
|
|
const material = mesh.material;
|
|
if (Array.isArray(material)) {
|
|
material.forEach((entry) => entry.dispose());
|
|
} else if (material) {
|
|
material.dispose();
|
|
}
|
|
});
|
|
|
|
this.baseColors = new WeakMap<Material, Color>();
|
|
}
|
|
|
|
static getFillRatio() {
|
|
return Math.min(this.deposited / FILL_TARGET, 1);
|
|
}
|
|
|
|
private static setupMaterials(zone: Object3D) {
|
|
zone.traverse((child) => {
|
|
if (!(child as Mesh).isMesh) return;
|
|
|
|
const mesh = child as Mesh;
|
|
const sourceMaterial = mesh.material;
|
|
if (Array.isArray(sourceMaterial)) return;
|
|
|
|
if (!sourceMaterial) return;
|
|
|
|
const cloned = sourceMaterial.clone();
|
|
if ("color" in cloned) {
|
|
this.baseColors.set(cloned, (cloned.color as Color).clone());
|
|
}
|
|
cloned.depthWrite = true;
|
|
cloned.polygonOffset = true;
|
|
cloned.polygonOffsetFactor = 2;
|
|
cloned.polygonOffsetUnits = 2;
|
|
mesh.material = cloned;
|
|
mesh.renderOrder = 0;
|
|
});
|
|
|
|
this.refreshFillVisual();
|
|
}
|
|
|
|
private static refreshFillVisual() {
|
|
if (!this.zoneRoot) return;
|
|
|
|
const ratio = this.getFillRatio();
|
|
|
|
this.zoneRoot.traverse((child) => {
|
|
if (!(child as Mesh).isMesh) return;
|
|
|
|
const material = (child as Mesh).material;
|
|
if (Array.isArray(material) || !material) return;
|
|
|
|
const baseColor = this.baseColors.get(material);
|
|
if (!baseColor || !("color" in material)) return;
|
|
|
|
(material.color as Color).copy(baseColor).lerp(FILL_COLOR, ratio);
|
|
});
|
|
}
|
|
}
|