Files
firstProj/src/controllers/Map/InteractiveZoneC.ts
T
2026-06-18 12:45:48 +03:00

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