Spaces:
Running
Running
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); | |
} | |
} |