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