victor's picture
victor HF Staff
Refactor tower system and UI improvements
9e6ef9c
import * as THREE from "three";
import {
UPGRADE_MAX_LEVEL,
UPGRADE_START_COST,
UPGRADE_COST_SCALE,
UPGRADE_RANGE_SCALE,
UPGRADE_RATE_SCALE,
UPGRADE_DAMAGE_SCALE,
SELL_REFUND_RATE,
} from "../../config/gameConfig.js";
import { KINDS } from "./kinds/index.js";
export class Tower {
constructor(pos, baseConfig, scene) {
this.position = pos.clone();
this.fireCooldown = 0;
this.scene = scene;
this.type = baseConfig.type || "basic";
this.projectileEffect = baseConfig.projectileEffect || null;
this.slowMultByLevel = baseConfig.slowMultByLevel || [0.75, 0.7, 0.65];
this.slowDuration = baseConfig.slowDuration || 1.5;
this.isElectric = this.type === "electric";
if (this.isElectric) {
this.maxTargets = baseConfig.maxTargets ?? 4;
this.damagePerSecond = baseConfig.damagePerSecond ?? 1;
this.visualRefreshRate = baseConfig.visualRefreshRate ?? 60;
this.visualRefreshInterval = 1 / Math.max(1, this.visualRefreshRate);
this.arcFadeDuration = baseConfig.arcFadeDuration ?? 0.2;
this.arcDurationMs = Math.max(
1,
baseConfig.arcDurationMs ?? this.arcFadeDuration * 1000
);
this.arcStyle = {
color: baseConfig.arc?.color ?? 0x9ad6ff,
coreColor: baseConfig.arc?.coreColor ?? 0xe6fbff,
thickness: baseConfig.arc?.thickness ?? 2,
jitter: baseConfig.arc?.jitter ?? 0.25,
segments: baseConfig.arc?.segments ?? 10,
};
this.targetPriority =
baseConfig.targetPriorityMode ||
baseConfig.targetPriority ||
"closestToExit";
}
this.level = 1;
this.baseRange = baseConfig.range;
this.baseRate = baseConfig.fireRate;
this.baseDamage = baseConfig.damage;
this.range = this.baseRange;
this.rate = this.baseRate;
this.damage = this.baseDamage;
if (this.type === "slow") {
const idx = Math.max(
0,
Math.min(this.slowMultByLevel.length - 1, this.level - 1)
);
const effect = this.projectileEffect || {};
this.projectileEffect = {
...effect,
type: "slow",
mult: this.slowMultByLevel[idx],
duration: this.slowDuration,
};
}
this.nextUpgradeCost = UPGRADE_START_COST;
this.totalSpent = baseConfig.cost;
this.isSniper = this.type === "sniper";
this.aimTime = baseConfig.aimTime ?? 0;
this.sniperProjectileSpeed = baseConfig.projectileSpeed ?? null;
this.cancelThreshold = baseConfig.cancelThreshold ?? this.range;
this.pierceChance = baseConfig.pierceChance ?? 0;
this.targetPriority = baseConfig.targetPriority || (this.isSniper ? "closestToExit" : "nearest");
this.aiming = false;
this.aimingTimer = 0;
this.aimedTarget = null;
this.laserLine = null;
const baseGeo = new THREE.CylinderGeometry(0.9, 1.2, 1, 12);
const baseMat = new THREE.MeshStandardMaterial({
color:
this.type === "slow" ? 0xff69b4 : this.isSniper ? 0x6d6f73 : 0x3a97ff,
metalness: this.isSniper ? 0.5 : 0.2,
roughness: this.isSniper ? 0.35 : 0.6,
});
const base = new THREE.Mesh(baseGeo, baseMat);
base.castShadow = true;
base.receiveShadow = true;
base.position.copy(this.position);
this.mesh = base;
this.baseMesh = base;
this.kind = KINDS[this.type] || KINDS.basic;
this.kind.buildHead(this, scene);
const ringGeo = new THREE.RingGeometry(this.range - 0.05, this.range, 48);
const ringMat = new THREE.MeshBasicMaterial({
color:
this.type === "slow" ? 0xff69b4 : this.isSniper ? 0x00ffff : 0x3a97ff,
transparent: true,
opacity: this.isSniper ? 0.32 : 0.2,
side: THREE.DoubleSide,
depthWrite: false,
depthTest: true,
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = -Math.PI / 2;
ring.position.y = 0.03;
base.add(ring);
ring.visible = true;
const outlineGeo = new THREE.TorusGeometry(1.05, 0.04, 8, 32);
const outlineMat = new THREE.MeshBasicMaterial({
color: 0xffff66,
transparent: true,
opacity: 0.85,
depthWrite: false,
});
const outline = new THREE.Mesh(outlineGeo, outlineMat);
outline.rotation.x = Math.PI / 2;
outline.position.y = 0.52;
outline.visible = false;
outline.name = "tower_hover_outline";
base.add(outline);
this.ring = ring;
this.hoverOutline = outline;
this.levelRing = null;
if (this.isElectric) {
this.applyVisualLevel();
}
scene.add(base);
}
get canUpgrade() {
return this.level < UPGRADE_MAX_LEVEL;
}
getSellValue() {
return Math.floor(this.totalSpent * SELL_REFUND_RATE);
}
upgrade() {
if (!this.canUpgrade) return false;
this.level += 1;
this.range *= UPGRADE_RANGE_SCALE;
this.rate *= UPGRADE_RATE_SCALE;
this.damage *= UPGRADE_DAMAGE_SCALE;
if (this.type === "slow") {
const idx = Math.max(
0,
Math.min(this.slowMultByLevel.length - 1, this.level - 1)
);
const effect = this.projectileEffect || {};
this.projectileEffect = {
...effect,
type: "slow",
mult: this.slowMultByLevel[idx],
duration: this.slowDuration,
};
}
if (this.isSniper) {
const minAimTime = (this.aimTime ?? 0) * 0.4;
this.aimTime = Math.max(minAimTime, (this.aimTime ?? 0) * 0.9);
this.pierceChance = Math.min(0.15, (this.pierceChance ?? 0) + 0.03);
}
const newGeo = new THREE.RingGeometry(this.range - 0.05, this.range, 48);
this.ring.geometry.dispose();
this.ring.geometry = newGeo;
this.totalSpent += this.nextUpgradeCost;
this.nextUpgradeCost = Math.round(
this.nextUpgradeCost * UPGRADE_COST_SCALE
);
this.applyVisualLevel();
return true;
}
applyVisualLevel() {
this.kind.applyVisualLevel(this);
}
tryFire(dt, enemies, projectiles, projectileSpeed) {
return this.kind.tryFire(this, dt, enemies, projectiles, projectileSpeed);
}
updateElectricArcs(now) {
if (this.kind.updateElectricArcs) {
this.kind.updateElectricArcs(this, now);
}
}
playShootSound() {
if (
typeof window !== "undefined" &&
window.UIManager &&
window.UIManager.playTowerSound
) {
try {
window.UIManager.playTowerSound(this);
} catch {}
}
}
playAimingTone() {
if (
typeof window !== "undefined" &&
window.UIManager &&
window.UIManager.playAimingTone
) {
try {
window.UIManager.playAimingTone(this);
} catch {}
}
}
stopAimingTone() {
if (
typeof window !== "undefined" &&
window.UIManager &&
window.UIManager.stopAimingTone
) {
try {
window.UIManager.stopAimingTone(this);
} catch {}
}
}
playFireCrack() {
if (
typeof window !== "undefined" &&
window.UIManager &&
window.UIManager.playFireCrack
) {
try {
window.UIManager.playFireCrack(this);
} catch {}
}
}
setSelected(selected) {
this.selected = !!selected;
if (this.hoverOutline) {
this.hoverOutline.visible = this.selected || !!this.hovered;
this.hoverOutline.material.opacity = this.selected ? 0.95 : 0.85;
}
}
setHovered(hovered) {
this.hovered = !!hovered;
if (this.hoverOutline) {
this.hoverOutline.visible = this.selected || this.hovered;
}
}
destroy() {
if (this.kind.onDestroy) {
this.kind.onDestroy(this);
}
if (this.levelRing) {
this.scene.remove(this.levelRing);
this.levelRing.geometry.dispose();
if (this.levelRing.material?.dispose) this.levelRing.material.dispose();
this.levelRing = null;
}
this.scene.remove(this.mesh);
}
}