User prompt
move it 10 more pixels up and 15 pixels left
User prompt
towers move actual tower place look 30 pixel up 20 pixel left
User prompt
fix it
User prompt
Fix tower preview positioning to match actual tower placement Fix tower placement positioning to match preview positioning
User prompt
match tower shadow and real look now towers place something like 20 pixels down and 20 pixels right i want you to math their locations
User prompt
1 · Ölçekleri sıfırla – tek sprite kullan Nerede Ne yap Tower sınıfı içinde baseGraphics baseGraphics.scale.set(1) veya scale satırlarını tamamen sil. Width/height zaten CELL_SIZE*BLOCK_SIZE (1.5× büyütülmüş) ise ek ölçek gerekmez. TowerPreview İkinci “shadow/ghost” sprite varsa kaldır. Sadece TEK Graphics/Sprite bırak. SourceTower ikonları (menüde) icon.scale.set(1) – tower boyutu menüde zaten küçük görünüyorsa sadece ikon tekstürü küçüktür; ölçekleme gerekmez. 2 · Pivot ve hizalama – gölge tam blok merkezinde dursun js Kopyala Düzenle const blockW = CELL_SIZE * BLOCK_SIZE; // artık 1.5× road’a eşit sprite.pivot.set(blockW/2, blockW/2); Hem TowerPreview hem Tower gövdesi için bu pivot ayarla. 3 · snapToGrid formülü (TowerPreview) js Kopyala Düzenle // (x,y) = pointer pozisyonu const gx = Math.floor( (x - grid.x) / CELL_SIZE ); // sol-üst hücre const gy = Math.floor( (y - grid.y) / CELL_SIZE ); preview.gridX = gx; preview.gridY = gy; // Ekrandaki konum = BLOK MERKEZİ preview.x = grid.x + (gx + BLOCK_SIZE/2) * CELL_SIZE; preview.y = grid.y + (gy + BLOCK_SIZE/2) * CELL_SIZE; TowerPreview’i “parmağın 1.5 hücre üstünde” tutuyorsan önce y’den CELL_SIZE*1.5 çıkar, sonra aynı formülü uygula. 4 · placement validation döngülerini güncelle Tüm tarama kontrolleri (çakışma, duvar, yol teması) için: js Kopyala Düzenle for (let dx = 0; dx < BLOCK_SIZE; dx++) // 0..BLOCK_SIZE-1 for (let dy = 0; dy < BLOCK_SIZE; dy++) { const c = grid.getCell(preview.gridX+dx, preview.gridY+dy); ... } Böylece “biraz yanına kaydırınca yeşile dönüyor” hatası gider. 5 · Tower yerleşim konumu js Kopyala Düzenle tower.x = grid.x + (gridX + BLOCK_SIZE/2) * CELL_SIZE; tower.y = grid.y + (gridY + BLOCK_SIZE/2) * CELL_SIZE; (Aynı formül; yerleştirince blok tam oturur). 6 · Debug katmanı hiyerarşisi scss Kopyala Düzenle game.addChild(debugLayer); // en önce game.addChild(roadLayer); game.addChild(towerLayer); // debug üstünde game.addChild(enemyLayer); Böylece oklar/sayılar tower gövdesinin altında kalır; TowerPreview ise towerLayer’ın üstünde olmalı. 7 · Hızlı doğrulama (AI kendisi denesin)
User prompt
set sizes of towers and roads 1.5 time bigger
User prompt
set sizes of towers same to road
User prompt
Aşağıdakileri tek paket talimat olarak Upit AI’ye ilet—böylece:
Merkezde spawn bloğu (2 × 2),
Altında 1 blokluk yol, onun soluna 3 blokluk yol,
En sondaki yol bloğu Base (type 3)
...şeklindeki başlangıç yerleşimi geri gelir ve düşmanlar yolu takip eder.
1 · Grid’i önce “duvar” ile doldur
js
Kopyala
Düzenle
for (let x=0; x
User prompt
Upit AI’ye – “Blok Boyutu Eşitle & Çift-Kutu Hayaleti Temizle”
Aşağıdaki değişiklikleri yalnızca bu dosya üzerinde yap; başka hiçbir yeri elleme. Sonra her şeyi derle – yol da kule de aynı boyutta, ekranda tek bir kutu görünecek.
1 · Tek kaynaktan boyut: BLOCK_SIZE = 2
js
Kopyala
Düzenle
const CELL_SIZE = 76; // dokunma
const BLOCK_SIZE = 2; // 2 × 2 hücre ⇐ yol & kule için **ortak**
Kodun geri kalanında hiç “2”, “3”, “5” gibi sihirli büyüklük sayısı kalmasın; daima BLOCK_SIZE kullan.
2 · Grafik eni–boyu & pivotu tek olsun
(Çift kutu görüntüsü buradan çıkıyor.)
Nesne Ne yap
tower, towerpreview asset’leri width = CELL_SIZE*BLOCK_SIZE
height = CELL_SIZE*BLOCK_SIZE
pivot.set(width/2, height/2)
RoadPreview Aynı ölçü/pivot; tek bir Graphics kullan (LK.getAsset(‘cell’...) + tint kahverengi).
Kule gövdesi (Tower içindeki baseGraphics) Eski scaleX=1.3 satırlarını sil; width/height yukarıdakiyle aynı bırak.
Artık her konteynerde yalnız bir görünür kare kalacak; “görünmeyen ikiz” kalkacak.
3 · Snap & yerleştirme merkezini düzelt
Snap formülü (RoadPreview & TowerPreview)
js
Kopyala
Düzenle
self.gridX = Math.floor((x - grid.x) / CELL_SIZE);
self.gridY = Math.floor((y - grid.y) / CELL_SIZE);
self.x = grid.x + (self.gridX + BLOCK_SIZE/2) * CELL_SIZE; // gridX + 1
self.y = grid.y + (self.gridY + BLOCK_SIZE/2) * CELL_SIZE;
TowerPreview için “parmağın üstünde göster” hilesi gerekiyorsa sadece y değerini y - CELL_SIZE*1.5 ile besle; formül aynı.
Yerleştirirken
Tower.placeOnGrid ve road ekleme döngüleri:
js
Kopyala
Düzenle
for(let dx=0; dx
User prompt
Aşağıdakileri tek paket talimat olarak Upit AI’ye ilet – artık hem Road hem Tower blokları kesinlikle 2 × 2 hücre olacak. (3 × 3 denemesi tutmadı; bu kez 2 × 2’ye geçiyoruz. Tüm “5 × 5” sorunlarını da böylece silip süpürmüş olacağız.) 0 · Genel sabit js Kopyala Düzenle const BLOCK_SIZE = 2; // 2 hücre ⇐ TEK kaynak const CELL = 76; // var olan kare pikseli Her yerde BLOCK_SIZE kullan, sihirli “2 / 1” sayıları bırakma. 1 · Grafik boyutları & pivot Nesne Kod RoadPreview / Road gövdesi js\nroadG.width = CELL * BLOCK_SIZE; // = 152 px\nroadG.height = CELL * BLOCK_SIZE;\nroadG.pivot.set( roadG.width/2, roadG.height/2 ); TowerPreview / Tower gövdesi Aynı üç satır (tower sprite için). 2 · “Sol-üst + orta” hizalama 2-a · Sol-üst hücre js Kopyala Düzenle gx = Math.floor((pointer.x - grid.x) / CELL); gy = Math.floor((pointer.y - grid.y) / CELL); 2-b · Ekran konumu (blok merkezi) js Kopyala Düzenle obj.x = grid.x + (gx + BLOCK_SIZE/2) * CELL; // gx + 1 (çünkü 2×2) obj.y = grid.y + (gy + BLOCK_SIZE/2) * CELL; // gy + 1 TowerPreview hâlâ parmağın 1.5 hücre üstünde gösterilecekse, önce y-offseti çıkar, sonra aynı formülü uygula. 3 · Yerleştirme döngüleri (2 hücre) Bütün Road/Tower tarama, yerleştirme, çakışma kontrolleri: js Kopyala Düzenle for (let dx = 0; dx < BLOCK_SIZE; dx++) // 0,1 for (let dy = 0; dy < BLOCK_SIZE; dy++) { const c = grid.getCell(gx+dx, gy+dy); ... } 4 · Road ekleme kuralları (2 × 2) Blok merkezi = gx+1 , gy+1 Base’in merkezi = pathPoints[pathPoints.length-1]. Yeni yol ancak Base bloğunun üç serbest yönünden birine (down, left, right / geri yön hariç) konulabilir. Mesafe kriteri: js Kopyala Düzenle Math.abs(nx - baseCx) + Math.abs(ny - baseCy) === BLOCK_SIZE (Çünkü bloklar 2 hücre geniş, merkezler arası mesafe tam 2 olur.) Yerleşince –5 gold, eski Base bloğundaki 4 hücre type 0, yeni blok 4 hücre type 3, pathPoints.push({cx,cy}), grid.pathFind(); renderDebug(); 5 · Tower yerleştirme kuralları (2 × 2) Tüm hedef hücreler type 1 && !hasTower. Yanındaki (4-yön) herhangi bir hücre type 0|2|3 olmalı. Yerleşince 4 hücre type 4, hasTower = true. 6 · Spawn & Base düzeltmeleri Spawn çıkışı = spawn’ın tam altında kalan tek hücre (spawn.x, spawn.y+1), tip 0. spawn.targets = [thatCell] – diğer komşuları listeye ekleme. Base yol dizisi artık merkezler listesi; A* cell.type !== 1 && !hasTower yürüme kuralı değişmez. 7 · Debug / pathPoints Ok numaraları ve yönleri blok merkezlerine (gx+1, gy+1) çizilmeye devam etsin; Road blokları 2×2 olduğundan sayılar tam ortada kalır. 8 · Artık kullanılmayan kod kalıntılarını temizle Bul → CELL*3, CELL*5, <=2, dx<=2, dy<=2, dx<=1 && BLOCK_SIZE==3 vb. Değiştir → CELL*BLOCK_SIZE veya <= BLOCK_SIZE-1. Hatalı pivot: pivot.set(CELL*2.5, ... ) → pivot.set(width/2, height/2).
User prompt
Upit AI’ye verilmesi gereken net fix listesi (Özet: yol, kule ve tüm ön-izleme blokları GERÇEKTEN 3 × 3 hücre olacak; “5 × 5” yanılgısı pivottaki ve snap hesabındaki çarpan hatasından kaynaklanıyor.) Adım Yapılacak değişiklik Neden 1 const ROAD_SIZE = 3; const TOWER_SIZE = 3; yalnız bu satırda tanımlı kalsın. Kodun geri kalanında bu sabitler yeniden atanmasın. 5 × 5’e “dönüş” ROAD_SIZE / TOWER_SIZE değişkeninin başka yerde 5 yapılmasından değil, aşağıdaki hatalı konum / ölçek formüllerinden kaynaklanıyor. 2 Ölçek graphics.width = CELL_SIZE * BLOCK_SIZE; graphics.height = CELL_SIZE * BLOCK_SIZE; (RoadPreview, TowerPreview, Tower gövdesi) Width/height zaten 3 × CELL olacak — böyle bırak. 3 Pivot (hem RoadPreview hem TowerPreview): graphics.pivot.set(graphics.width/2, graphics.height/2); Merkezden döndürme/hizalama için. 4 SnapToGrid – TOP-LEFT koordinatı (en kritik satır!) // pointer (x,y) → sol-üst hücre: self.gridX = Math.floor((x - grid.x) / CELL_SIZE) - 1; self.gridY = Math.floor((y - grid.y) / CELL_SIZE) - 1; // ekran konumu (blok merkezi) self.x = grid.x + (self.gridX + 1.5)*CELL_SIZE; self.y = grid.y + (self.gridY + 1.5)*CELL_SIZE; Eskiden + CELL_SIZE * ROAD_SIZE / 2 ekleniyordu; bu, gridX’i “merkez” sandığı için 3×3 blokları 1.5 hücre sağ-aşağı kaydırıyor, 5 × 5 görünüme sebep oluyordu. Yeni formül: gridX/gridY = sol-üst, merkez = +1.5 CELL. 5 Yerleştirme döngüleri – her yerde −1...+1 yerine for(dx=0; dx<3; dx++) & for(dy=0; dy<3; dy++) kullan. gridX/gridY artık sol-üst; döngüler 0-1-2 hücreyi işaretler. 6 Tower.placeOnGrid ve Road yerleştirme kodu: self.x = grid.x + (gridX + 1.5)*CELL_SIZE; self.y = grid.y + (gridY + 1.5)*CELL_SIZE; Aynı “sol-üst + 1.5” düzeltmesi. 7 updatePlacementStatus (Road & Tower) ‣ Geçerli hücre taraması: for(dx=0; dx<3; dx++) for(dy=0; dy<3; dy++) ... ‣ Yolun Base’e bitişik mi kontrolü: Base’in merkez koordinatı ile gridX+1, gridY+1 karşılaştır. 3 × 3 bloğun ortası dikkate alınmalı. 8 Debug & pathPoints — Base/Yol eklerken diziye blok merkezi (gridX+1, gridY+1) ekleyin; pathFind ve ok yönleri doğru kalır. Konum sistemi değiştiği için. 9 Arayın & Temizleyin – kodda hâlâ: CELL_SIZE * 5, scaleX = 5, <= 2 gibi 5/±2 geçen eski kalıntılar varsa *3, <= 1 ile değiştirin. Yanlış ölçekteki tek tük satırlar sorunu geri getirmesin. 10 Hızlı doğrulama (AI test) 1. RoadPreview 3 × 3 kahverengi kare, tam parmak altında. 2. TowerPreview 3 × 3 gri/yeşil kare. 3. Yerleştirince hem yol hem kule tam 3 × 3 hücre kaplar; debug sayılar taşmaz. 4. Hiçbir 5 × 5 blok veya kayma kalmaz.
User prompt
Aşağıdakileri tek seferde Upit AI’ye uygulat—yol, kule ve ön-izleme blokları tam olarak 3 × 3 hücre (CELL_SIZE × 3) olacak; hiçbir 5 × 5 kalmayacak. 1 · Tek kaynak: BLOCK_SIZE = 3 js Kopyala Düzenle const BLOCK_SIZE = 3; // 3 hücre const CELL = 76; // zaten vardı Başka hiçbir yerde “3”, “5” gibi sihirli sayı KULLANMA; hep BLOCK_SIZE. 2 · Grafiklerin gerçek boyutu Nesne Kodda yap RoadPreview & Road blok graphics.width = CELL*BLOCK_SIZE; graphics.height = CELL*BLOCK_SIZE; graphics.pivot.set(width/2, height/2); TowerPreview & Tower gövdesi Aynı üç satır, graphics yerine tower sprite. DebugCell Değiştirme—tek hücre olarak kalacak. Önceki kodda hâlâ CELL*5 veya scale.set(5,5) kalan satırlar var—sil veya *BLOCK_SIZE yap. 3 · Yerleştirme döngülerini 3’e sabitle js Kopyala Düzenle for (let dx = -1; dx <= 1; dx++) // RoadPaint & TowerPlace for (let dy = -1; dy <= 1; dy++) { var c = grid.getCell(cx+dx, cy+dy); ... } Hiçbir -2 ... +2 döngüsü kalmayacak. 4 · Hizalama (snap) kesin formül js Kopyala Düzenle obj.x = grid.x + gx*CELL + (CELL*BLOCK_SIZE)/2; obj.y = grid.y + gy*CELL + (CELL*BLOCK_SIZE)/2; gx, gy = sol-üst hücre koordinatı. Preview’de offset ekleme (tower’deki –CELL1.5 vb.) yalnız tower için geçerli; road için sıfır offset.* 5 · Kare seçimi Mouse’u hücre merkezine yuvarla: js Kopyala Düzenle gx = Math.floor((pointer.x - grid.x) / CELL); gy = Math.floor((pointer.y - grid.y) / CELL); TowerPreview’de y-offset varsa önce çıkar, sonra aynı formül. 6 · Kural döngüleri updatePlacementStatus() kontrolleri daima js Kopyala Düzenle for (let dx = 0; dx < BLOCK_SIZE; dx++) for (let dy = 0; dy < BLOCK_SIZE; dy++) şeklinde dönsün. 7 · Serseri 5’leri avla Projenin tüm dosyalarında arat: markdown Kopyala Düzenle *CELL*5 scale.*(5 for.*<= ?2 Bunları *BLOCK_SIZE ve <=1 (-1...+1) ile değiştir.
User prompt
Upit AI’ye aktar – “Gerçek 3 × 3” düzeltmesi (Amaç : yol ve kule blokları kesinlikle 3 hücre × 3 hücre olsun; RoadPreview/TowerPreview hücre merkezine tam otursun. Şu an 5 × 5 görünmesinin nedeni pivot/eni ve konum hesaplarının hatalı olması.) 1 · Sabitler doğru: js Kopyala Düzenle const ROAD_SIZE = 3; const TOWER_SIZE = 3; Bunlara dokunma – ama bütün genişlik-yükseklik ve konumlamalar bu sabitlere göre yeniden ayarlanmalı. 2 · Asset boyutlarını 3 × CELL_SIZE yap Başlangıçta tanımlanan şekiller js Kopyala Düzenle LK.init.shape('tower', {width: CELL_SIZE*3, height: CELL_SIZE*3, color:0xffffff, shape:'box'}); LK.init.shape('towerpreview',{width: CELL_SIZE*3, height: CELL_SIZE*3, color:0xffffff, shape:'box'}); Önceden 152 px (2 × CELL) idi – mutlaka 228 px (= 3 × 76) olsun. Kaynak ikonları (SourceTower/RoadSource) – baseGraphics.scaleX = baseGraphics.scaleY = 1; – ikonlar zaten menüde; ölçek büyütmesi gereksiz. 3 · Preview pivot & hizalama 3-a · RoadPreview js Kopyala Düzenle roadGraphics.width = CELL_SIZE * ROAD_SIZE; roadGraphics.height = CELL_SIZE * ROAD_SIZE; roadGraphics.pivot.set( roadGraphics.width/2, roadGraphics.height/2 ); // << DOĞRU PİVOT ... self.snapToGrid = (x,y)=>{ self.gridX = Math.floor((x-grid.x)/CELL_SIZE); self.gridY = Math.floor((y-grid.y)/CELL_SIZE); self.x = grid.x + self.gridX*CELL_SIZE + CELL_SIZE/2; self.y = grid.y + self.gridY*CELL_SIZE + CELL_SIZE/2; self.updatePlacementStatus(); }; (Artık kare, parmağın tam altında ve 3 × 3 ölçeğinde görünür.) 3-b · TowerPreview Benzer şekilde: js Kopyala Düzenle previewGraphics.width = CELL_SIZE * TOWER_SIZE; previewGraphics.height = CELL_SIZE * TOWER_SIZE; previewGraphics.pivot.set( previewGraphics.width/2, previewGraphics.height/2 ); ... self.snapToGrid = (x,y)=>{ self.gridX = Math.floor((x-grid.x)/CELL_SIZE); self.gridY = Math.floor(((y-CELL_SIZE*1.5)-grid.y)/CELL_SIZE); // 1.5 tile yukarı gösterim hâlâ geçerli self.x = grid.x + self.gridX*CELL_SIZE + CELL_SIZE/2; self.y = grid.y + self.gridY*CELL_SIZE + CELL_SIZE/2; self.checkPlacement(); }; 4 · Yerleştirme hesapları 4-a · Kule placeOnGrid js Kopyala Düzenle self.x = grid.x + gridX*CELL_SIZE + (CELL_SIZE*TOWER_SIZE)/2; self.y = grid.y + gridY*CELL_SIZE + (CELL_SIZE*TOWER_SIZE)/2; 4-b · Road eklerken Road bloğunu 3 × 3 işaretlemek için döngü zaten -1...+1; -2...+2 kullanan yer varsa değiştir. 5 · Collision / can-place kontrolleri TowerPreview.updatePlacementStatus: döngüler for (i=0;i@upit/tween.v1 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
A · RoadPreview Hizası (“uçan kare” problemi) Tek doku, tek pivot js Kopyala Düzenle roadPreview = new PIXI.Graphics() .beginFill(0x8B4513, 0.5) .drawRect(0, 0, CELL, CELL) .endFill(); roadPreview.pivot.set(CELL/2, CELL/2); Snapping — sadece merkez-hücreye hizala, offset KULLANMA: js Kopyala Düzenle roadPreview.x = grid.x + gx*CELL + CELL/2; roadPreview.y = grid.y + gy*CELL + CELL/2; Preview katmanı towerLayer üstünde, debugLayer altında; tint = 0x00ff00 / 0xff0000, alpha tween 0.4↔0.8. B · Yeni Mod: “3 × 3” Yol ve Kule B-1 — Sabitler js Kopyala Düzenle const ROAD_SIZE = 3; // kare sayısı const TOWER_SIZE = 3; Grid hücre boyutu (CELL) aynı kalır. B-2 — Yol oluşturma & yerleştirme Başlangıç yolu Spawn (1 hücre) → 1 DOWN (3×3 yol bloğu) → 3 LEFT (her adım 3×3). Her 3×3 bloğun merkez hücresi pathPoints dizisine girer. Road eklerken İstenen merkez hücre duvar olmalı (type 1, !hasTower). Merkez, Base bloğunun üç serbest yönünden birinde olmalı (geri yön hariç). Yerleşim onaylanınca: o merkezin etrafındaki 3×3 dokuz hücre js Kopyala Düzenle for(nx = cx-1; nx<=cx+1; nx++) for(ny = cy-1; ny<=cy+1; ny++){ var c = grid.getCell(nx,ny); c.type = 0; // yol c.hasTower = false; } Eski Base bloğunun hücreleri type 0, yeni bloğun hücreleri type 3. Altın −5 → pathPoints.push({cx,cy}) → grid.pathFind() → renderDebug(). B-3 — Kule yerleştirme Preview canPlace Kontrol edilecek dikdörtgen = TOWER_SIZE × TOWER_SIZE, yani 3×3. Tüm hücreler type 1 && !hasTower. Dikdörtgenin en az bir kenar hücresi, 4-yön komşu olarak type 0 | 2 | 3. Yerleştirirken her 3×3 hücre: js Kopyala Düzenle cell.type = 4; // tower-body cell.hasTower = true; Kule gölgesi 3×3 grafik + pulse; tint yeşil/kırmızı. B-4 — Path-finding A* zaten “cell.type !== 1 && !hasTower yürünebilir” diye çalışıyorsa değişiklik yok; Road bloğunun tamamı artık type 0. Spawn’ın targets dizisi hâlâ sadece DOWN-merkez hücreyi içersin, böylece düşman önce aşağı hareket eder. C · Ek korumalar Durum Eylem Road bloğu Base’e değmeden, ya da geri yönde Preview kırmızı + “Road must connect to Base (front/sides)!” Road bloğu spawn’a komşu Reddedilsin Kule bloğu yol ya da başka kule ile çakışır Reddedilsin Altın < 5 (road) / < biaya (kule) Reddedilsin; ikon gri ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
1 · RoadPreview yanlış “kırmızı/yeşil” gösteriyor Sebep: canPlace kontrolü şu iki durumu ayırt edemiyor pointer kareyle aynı hücre içinde ama “bitik yol (type 0)” üstünde ⇒ cell.type ≠ 1 → kırmızı pointer piksel olarak hücrenin sınırında ⇒ bir önceki (gerçek duvar) hücre hesaplanıyor → yeşil Çözüm js Kopyala Düzenle // Road sadece DUVAR (type 1) hücresine konabilir if (!cell || cell.type !== 1 || cell.hasTower) { self.canPlace = false; return; } Hiçbir koşulda type 0/2/3 üstüne yeni road konamaz. 2 · “Sadece Base’e bitişik 3 yön” kuralı Son Base hücresini al → var base = pathPoints[pathPoints.length-1] dirs = [[0,-1],[1,0],[0,1],[-1,0]] dizisiyle dört komşu üret Sadece bu dört duvar-hücreden biriyse → ✓ Geri yöndeki hücre (önceki ↔ yeni ters) → ✗ Spawn’a komşuluk → ✗ (Mevcut kodda bu mantık var, ancak 1. adımın cell.type filtresi yüzünden geçersiz kalıyordu.) 3 · Snap hizası – “uçan kare” sorununu bitir RoadPreview pivot zaten merkezde. snapToGrid tek satır olsun: js Kopyala Düzenle self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE/2; self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE/2; game.move ’de Y-offset YOK (towerPreview’deki -CELL_SIZE*1.5 sadece kuleye ait). Preview katmanı towerLayer’ın üstünde olsun; debug okları altta kalsın. 4 · Road’u “yol-yanına yol” yasakla Ek kontrol: js Kopyala Düzenle var dirsRoad = [[0,-1],[1,0],[0,1],[-1,0]]; for (var d=0; d<4; d++){ var n = grid.getCell(self.gridX+dirsRoad[d][0], self.gridY+dirsRoad[d][1]); if (n && n.type===0) { // bitişik ROAD tespit self.canPlace = false; break; } } Böylece yol yalnızca Base’e (type 3) eklenebilir; bitişik başka road varsa reddedilir. 5 · Görsel netlik Preview rengi tint = 0x00ff00 (yeşil) ↔ 0xff0000 (kırmızı) yalnız canPlace durumuna göre. Pulse tween preview üzerinde kalır (alpha 0.4 ↔ 0.8). ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Aşağıdaki maddeler, “Spawn’dan çıkan düşman yalnızca altındaki ilk yol hücresine yönelsin; Spawn’ın sağ/sol/üst komşuları yol bile olsa dikkate alınmasın” kuralını kesin olarak uygular. Kodun nasıl yazılacağına Upit AI karar versin; burada yalnızca yapılması gereken değişiklikler anlatılıyor. 1 · Spawn’ın “tek çıkış şeridi” kuralı Gereken Yapılacak İlk hedef sabit Spawn (type 2) daima [0,+1] yönündeki hücreyi ilk hedef kabul eder. Yan-yollara kilit Spawn’ın sağ/sol/üst komşuları yürünebilir kabul edilmemeli—even if they are type 0. Önerilen uygulama (basit) Spawn çıkış hücresini (spawnExit) baştan tanımla: js Kopyala Düzenle const spawnExit = grid.getCell(spawn.x, spawn.y + 1); Bu hücre spawn kurulumunda zaten yol (type 0) yapılmış oluyor. Enemy ilk hedefi yaratılırken doğrudan bu hücre olsun: js Kopyala Düzenle enemy.currentTarget = spawnExit; ve enemy.cellPathIndex = 1; gibi bir sayaçla sıradaki hedefi pathPoints[enemy.cellPathIndex] dizisinden çek. grid.pathFind() hâlâ spawn→base arası ok yönlerini üretmeye devam edebilir; ama spawn hücresinin targets dizisini manuel olarak şu tek hücre ile sınırla: js Kopyala Düzenle spawn.targets = [spawnExit]; 2 · Spawn’a bitişik yol koymayı yasakla RoadPreview.updatePlacementStatus() içinde ek kural: js Kopyala Düzenle // spawn.x, spawn.y global değişken var touchingSpawn = (Math.abs(cell.x - spawn.x) + Math.abs(cell.y - spawn.y)) === 1; if (touchingSpawn) canPlace = false; Böylece oyuncu spawn’ın sağ/sol/üst yanına yol ekleyemez; yalnızca spawn->down hattı başlangıç kalır. 3 · Düşman rotası pathPoints sırası ile yürüsün Enemy’nin güncellemesinde: js Kopyala Düzenle if (!enemy.currentTarget) { enemy.cellPathIndex = 1; // ilk hedef spawnExit enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } if (enemy.reachedCurrentTarget()) { enemy.cellPathIndex++; enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } Avantaj: Yol uzayınca sadece Base değişiyor; yolun ilk segmenti (spawn→spawnExit) hep aynı kalıyor, ara dizinler büyüyor. 4 · Yeni yol eklenince pathPoints dizisi uzuyor, spawn kısmı değişmez Yol eklenirken: js Kopyala Düzenle pathPoints.push({x: cell.x, y: cell.y}); // sadece sona ekle Spawn ve ilk yol noktası dizide sabit kaldığından mevcut düşmanların cellPathIndex bozulmaz.
User prompt
· RoadPreview sadece “yarı saydam kahverengi kare” olmalı Şu anda debug path-id sprite’ı klonladığın için +2 +3 +4 +5 etiketleri de kopyalanıp taşınıyor – bu yüzden kare garip görünüyor. RoadPreview’ı tamamen ayrı bir grafik olarak oluştur: js Kopyala Düzenle const roadPreview = new PIXI.Graphics(); roadPreview.beginFill(0x8B4513, 0.5); // kahverengi, %50 şeffaf roadPreview.drawRect(0, 0, CELL_SIZE, CELL_SIZE); roadPreview.endFill(); roadPreview.pivot.set(CELL_SIZE/2, CELL_SIZE/2); // merkez pivot Tween’le 0.4 ↔ 0.8 alpha “pulsing” efekti uygula (TowerPreview’deki metodun aynısı). 2 · Kafadan hizalama düzeltmesi snapToGrid(gx, gy) içinde pozisyonu daima: js Kopyala Düzenle roadPreview.x = grid.x + cellX * CELL_SIZE + CELL_SIZE/2; roadPreview.y = grid.y + cellY * CELL_SIZE + CELL_SIZE/2; Ekstra −CELL_SIZE × 1.5, +offset vs. gibi kaydırmalar kullanma; yalnız merkez-hücre hizası. 3 · Ön-izleme “yeşil/kırmızı” rengi Yerleştirilebilir → roadPreview.tint = 0x00ff00 (yeşil). Yerleştirilemez → roadPreview.tint = 0xff0000 (kırmızı). (Alpha tween yine çalışır; yalnız renk değişir.) 4 · Debug katmanından ayrı tut RoadPreview ve TowerPreview oyunLayer içinde, debug oklarını çizen debugLayer’dan üstte olsun; böylece sayılar/oklar preview’in üzerine taşmaz. 5 · Gizle / göster yönetimi roadPreview.visible = self.canPlace; – yerleştirilemezse kırmızı kareyi yine göster ama canPlace false ise yarım saniyede fade-out ile kaybolsun (ister Tween ister alpha -= dt × X). ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
1 · Road Simgesini (kaynağını) gerçek “sürükle-bırak” nesnesi yap Nerede Değişiklik Kaynak ikonları oluşturulurken (createSourceButtons ya da benzeri) roadSource.interactive = true; roadSource.buttonMode = true; ayarla. roadSource.data = {type:"road"}; (Tower ikonlarında kullanılanla aynı alan). game.down (oyuncu ekrana bastığında) Mevcut kontrol sadece sourceTower.type==="tower" ise TowerPreview başlatıyor. • else if (source.type === "road") dalı ekle → RoadPreview yarat. game.move & game.up TowerPreview için yazılan kodu RoadPreview ile de çağır. Yani if (roadPreview) roadPreview.snapToGrid(...) ve roadPreview.tryPlace() bloklarını ekle. Sonuç: Road ikonu artık Default/Rapid/Splash simgeleriyle aynı davranışı gösterir: bas-sürükle-bırak. 2 · Road yerleştirme kurallarını güncelle 2.1 – Konulabilir mi? Hedef hücre duvar olmalı → cell.type === 1 ve !cell.hasTower. Hücre Base’in (yolun son noktası) üç yanından biri olmalı: Base = pathPoints[pathPoints.length-1] Gelen_Dir = Base − Önceki_Kare Koyulmak_İstenen_Dir = Hedef − Base Koyulmak_İstenen_Dir !== −Gelen_Dir olmalı (back-tracking yasak). Altın ≥ 5 olmalı. Bu kontrolü RoadPreview.updatePlacementStatus() içinde uygula; sağlansa yeşil yanıp sönme, sağlanmazsa kırmızı. 2.2 – Yerleştirildiğinde yapılacaklar js Kopyala Düzenle setGold(gold - 5); // 1) Altın düş var oldBase = grid.goals[0]; oldBase.type = 0; // 2) Eski base artık yol // 3) Yeni hücre base oluyor cell.type = 3; grid.goals[0] = cell; pathPoints.push({x:cell.x, y:cell.y}); // 4) Yol dizisine ekle grid.pathFind(); // 5) Yeni yol yönlerini hesapla grid.renderDebug(); // 6) Okları güncelle flyUpText("-5 G", cell.x, cell.y); 3 · Çakışmadan kule yerleşimini koru Önceki talimatlardaki “4 hücreyi type 4 / hasTower=true işaretle” ve “canPlace, touchesRoad” kontrolleri aynen kalacak; Road kodundaki güncellemelerle çelişmez. 4 · UI / Bildirim Road palet etiketi “Road\n5”. Başarısız yerleştirme mesajları: Altın az → “Not enough gold!” Bitişik değil → “Road must connect to Base!” Geri yönde → “Road can’t go backwards!”
User prompt
Aşağıdaki talimatı Upit AI’ye ilet – yol parçası artık gerçekten konulabilir ve koyulduğunda yolu uzatır. (Önceki kodda “yol ekleme” yalnızca hücrenin type değerini 0 yapıyor ama Base’i ilerletmediği için düşman rotası hiç değişmiyor; ayrıca oyuncuda 5 gold yoksa sessizce reddediliyor.) “Road-Placement Hot-Fix” – yapılacak işler (özet) 1 · Yerleştirme koşulu Hücre type === 1 (duvar) ve !hasTower olmalı. 4-yön komşularından en az biri type 0 | 2 | 3 (yani mevcut yol/spawn/base). Oyuncunun ≥ 5 gold’u olmalı. Kodda bu mantık zaten RoadPreview.updatePlacementStatus() içinde var; sadece şu iki ayrıntı eklensin: js Kopyala Düzenle // 2-a) diyagonal komşu yol varsa ama ortogonal yoksa, canPlace = false // 2-b) gold < ROAD_COST ise self.canPlace = false (şu anda sadece renk değişiyor) 2 · Base’i ileri taşı Başarılı yol yerleştirilince (game.up içinde): js Kopyala Düzenle // 2-1) Altını düş setGold(gold - ROAD_COST); // 2-2) Eski base’i normale çevir var oldBase = grid.goals[0]; oldBase.type = 0; // artık sıradan yol // 2-3) Yeni hücreyi base yap cell.type = 3; grid.goals[0] = cell; // tek hedef hep en sondaki kare // 2-4) pathPoints dizisini güncelle (debug oklar için) pathPoints.push({x: cell.x, y: cell.y}); // 2-5) Patikayı yeniden hesapla / oku yeniden çiz grid.pathFind(); grid.renderDebug(); 3 · Kullanıcı geribildirimi Başarılı ekleme → “−5 G” uçan yazı. Başarısız ekleme → Gold yetersiz → “Not enough gold!” Komşu yok → “Road must connect to existing path!” 4 · Ön-izleme ofsetini kaldır roadPreview.snapToGrid() çağrıları y- offset’siz çalışsın; kulelerdeki “parmağın üstünde göster” davranışı burada gereksiz karmaşa yaratıyor: js Kopyala Düzenle // game.down & game.move roadPreview.snapToGrid(x, y); // eski y - CELL_SIZE*1.5 yer almayacak
User prompt
1 · Kuleler Asla İç İçe Geçmesin
Gereken Yapılacak
Hücre işgali Kule yerleştirildiğinde kapladığı 4 hücreyi (2 × 2) “dolu” işaretle.
js
cell.type = 4; // ‘tower’ tipi
cell.hasTower = true; // ek bayrak (okunabilirlik için)
Ön-izleme kontrolü TowerPreview.updatePlacementStatus() içinde: 1) hedef 4 hücrenin tamamı cell.type === 1 ve !cell.hasTower; 2) bu 4 hücreden en az biri 4-yön komşuda type === 0, 2 veya 3 (yani yol/spawn/base).
Koşul sağlanmazsa self.canPlace = false.
Kule yerleşirken Placement onaylandığında ilgili hücrelerin type = 4 ve hasTower = true olarak set edilmesini unutma.
TowerPreview gölgesi Yeşil (yerleşebilir) / Kırmızı (yerleşemez) yanıp-sönme yalnızca canPlace=true/false durumlarında görünsün; mevcut alpha tween kullanılabilir.
2 · Road Eklemek Kolay ve Doğru Olsun
2.1 – Basit kural seti
Yalnızca type 1 (duvar) hücresine road konulabilir.
Yol karesinin 4-yön komşularından en az biri type === 0, 2 veya 3 olmalı (yani mevcut yol, spawn, ya da base).
Hücrede hasTower veya başka road varsa yerleştirme reddedilir.
Maliyet: 5 gold (UI ve kontroller tam uyacak).
Not: Duvarı yola çevirdiğin için path’i “kırma” riski yok; yeni road otomatik genişletme işlevi görüyor.
2.2 – Ön-izleme & bildirim
RoadPreview.updatePlacementStatus() yukarıdaki kurallara göre yeşil/kırmızı gölge gösterir.
Yanıp-sönen alpha tween RoadPreview için de kullan.
Başarılıysa “−5 G” fly-up; başarısızsa “Not adjacent to road!” veya “Gold < 5!” uyarısı.
2.3 – Yerleşim sonrası
js
Kopyala
Düzenle
cell.type = 0; // road
cell.hasTower = false; // garanti
grid.pathFind(); // spawn→base yeni yol puanları
grid.renderDebug(); // oklar güncellensin
3 · type Sabitleri Tablosu (öneri)
type Anlam Renk (debug)
0 Road kahverengi
1 Wall / boş gri
2 Spawn mavi
3 Base (goal) yeşil
4 Tower body koyu gri
Bu tablo koda açıklık katar; debug render’da da renklendir.
4 · Hata Mesajı Temizliği
“Cannot build here!” yalnızca cell.type ≠ 1 veya hasTower → kule reddi.
“Not adjacent to road!” yalnızca road yerleşiminde komşu kuralı sağlanmazsa.
“Tower would block path!” mesajı artık tamamen kaldırılabilir (duvarı yola çevirmiyoruz, tersi).
User prompt
1 · ROAD MEKANİĞİ Gereken Şu an Yapılacak Maliyet 10 gold ROAD_COST = 5 sabiti oluştur; • bütün “>= 10”, “-10” kontrollerini ve UI etiketini 5’e güncelle. Road konulabilir mi? RoadPreview.updatePlacementStatus() sadece gold >= 10 diyor gold >= ROAD_COST kullan. Altın düşme animasyonu “-10 G” Yazıyı “-5 G” yap. 2 · TOWER YERLEŞİM KURALI “Kuleler yolun kenarına (bitşik) konulabilsin, yol üstüne konulamasın.” 2.1 – Tip doğrulaması TowerPreview.updatePlacementStatus() şu satırı değiştir: js Kopyala Düzenle if (!cell || cell.type !== 0) { ... } Olması gereken: js Kopyala Düzenle // Kule ancak DUVAR (type 1) hücresine oturur if (!cell || cell.type !== 1) { ... } Aynı kontrol 2×2’lik tüm hücreler için geçer. 2.2 – Yola komşu mu? Ek koşul ekle: js Kopyala Düzenle var touchesRoad = false; var dirs = [[0,-1],[1,0],[0,1],[-1,0]]; // 4 yön for (var tx = 0; tx < 2; tx++){ for (var ty = 0; ty < 2; ty++){ for (var d=0; d<4; d++){ var nx = self.gridX+tx+dirs[d][0]; var ny = self.gridY+ty+dirs[d][1]; var nCell = grid.getCell(nx,ny); if (nCell && (nCell.type===0 || nCell.type===2 || nCell.type===3)){ touchesRoad = true; } } } } self.canPlace = validGridPlacement && touchesRoad && !self.blockedByEnemy; Sonuç: 2×2 kule bloğunun en az bir kenarı yol / spawn / base karşısına denk gelmiyorsa canPlace = false. 2.3 – Ön-izleme gölgesi Kule ön-izlemesi (TowerPreview) ‟gölge” olarak adlandırılan yarı-saydam kare: tween(rangeGraphics,{alpha:.5},...); benzeri döngüyle 0.4 ↔ 0.8 arası “pulsing” yap; sadece self.visible iken. 3 · ROAD YERLEŞİM GÖLGESİ RoadPreview zaten yarı saydam kahverengi; buna da aynı 0.4 ↔ 0.8 “pulsing” efekti ekle ki oyuncu road’u nereye koyabileceğini anlasın. 4 · wouldBlockPath() ARTIK GEREKSİZ Yol = type 0, düşmanlar sadece 0/2/3 üzerinde yürüdüğü için kule hücreleri (type 1) zaten path’i kapatamaz. js Kopyala Düzenle function wouldBlockPath(){ return false; } İster kaldır, ister bu şekilde bırak. 5 · UI ETİKETLERİ Build paletindeki Road etiketi “Road \n5” RoadPreview/Notification yazıları “10” yerine “5” roadSource.update() de gold>=ROAD_COST ile opacity ayarla.
User prompt
Şu an pathPoints dizininde sağlıklı adım sayısı yok; (12,15)ten doğrudan (9,15)e atlarken (11,15) ve (10,15) duvar kalıyor. js Kopyala Düzenle // YANLIŞ – iki hücre eksik [ {12,14}, {12,15}, {9,15} ] Yöntem: segment segment ilerleyen bir yardımcı oluştur. js Kopyala Düzenle function buildPath(spawnX, spawnY) { const pts = []; pts.push({x:spawnX, y:spawnY}); // spawn pts.push({x:spawnX, y:spawnY+1}); // 1 ↓ for (let k = 1; k <= 3; k++) { // 3 ← pts.push({x:spawnX-k, y:spawnY+1}); } return pts; // son eleman base } const pathPoints = buildPath(12, 14); Her nokta sırayla işlenirken: js Kopyala Düzenle if (i===0) { cell.type = 2; grid.spawns=[cell]; } // spawn else if (i===pathPoints.length-1) { cell.type = 3; grid.goals=[cell]; } // base/goal else { cell.type = 0; } // yol Sonra mutlaka: js Kopyala Düzenle grid.pathFind(); grid.renderDebug(); 2. Spawn hücresi görünsün DebugCell.render() içinde case 2 şu an case 0 ile ortak palette. Gözle fark edilmesi için benzersiz bir renk ver: js Kopyala Düzenle case 2: { // spawn self.removeArrows(); cellGraphics.tint = 0x4444ff; // mavi numberLabel.visible = false; break; } 3. Düşman doğuş koordinatı Şu satır düşmanı spawn hücresinin üstüne koyuyor: js Kopyala Düzenle var spawnY = spawnPoint.y - 1 - Math.random()*3; • -1 ve rasgele offseti kaldır. js Kopyala Düzenle enemy.cellX = spawnPoint.x; enemy.cellY = spawnPoint.y; enemy.currentCellX = spawnPoint.x; enemy.currentCellY = spawnPoint.y; Görüntü için: js Kopyala Düzenle enemy.x = grid.x + spawnPoint.x*CELL_SIZE + CELL_SIZE/2; enemy.y = grid.y + spawnPoint.y*CELL_SIZE + CELL_SIZE/2; 4. Road yerleştirince path güncellemesi Road karesi cell.type = 0 yapıldıktan hemen sonra: js Kopyala Düzenle grid.pathFind(); grid.renderDebug(); grid.goals zaten Base’i içeriyor, değişme yok. 5. Hızlı kontrol listesi Spawn (type 2) mavi kare olarak görünür. Base (type 3) yeşildir. pathFind() çağrısı sonrası spawn.pathId === pathId olmalı; konsolda “Spawn blocked” uyarısı kalmaz. Düşmanlar doğar doğmaz yolu takip eder.
User prompt
Spawn’dan çıkınca düşmanların gideceği hedef (goal) hücresi Path-Finding’e hiç tanıtılmadığı için grid.pathFind() boş çalışıyor. Hedefi bilmediği için de her hücrede cell.targets dizisi boş kalıyor; updateEnemy() içinde js Kopyala Düzenle if (!enemy.currentTarget) enemy.currentTarget = cell.targets[0]; // undefined olduğu anda düşman takılıyor ve pozisyonu güncellenmiyor. Upit AI’nin yapması gereken düzeltmeler Nerede (satır / blok) Ne yapmalı Sebep Path oluşturma bölümü (başlangıçta pathPoints döngüsü) Yolu sonlandırdığın kareyi (type = 3 olan Base) grid.goals dizisine it: js\nif (i === pathPoints.length-1) {\n cell.type = 3; // goal\n grid.goals.push(cell); // <<< eklenecek\n}\n grid.pathFind() propagasyonu için en az bir Goal gerekir. Aynı blok bittiğinde grid.pathFind() ve grid.renderDebug() hemen tekrar çağır (Goal eklendikten sonra). Hedef eklendiği anda yol puanları yeniden hesaplansın. Road yerleştirildiğinde (game.up içinde road yerleştirme parçası) Yeni hücreyi yol yapıp altını düşürdükten sonra mutlaka js\ngrid.pathFind();\n çağrısının önünde: js\ngrid.goals = [grid.goals[0]]; // Base hâlâ aynı; başka goal yoksa tek eleman kalsın\n Yolu uzattıktan sonra path tekrar hesaplansın; goals’ı sıfırlayıp sadece Base’i bırakmak kafa karışıklığını önler. updateEnemy (normal zemin düşmanları) js\nif (!enemy.currentTarget && cell.targets.length)\n enemy.currentTarget = cell.targets[0];\n Cell hedef listesi boşsa undefined atamasın diye ek koruma. Sonuç Base hücresi artık grid.goals içinde olacağı için: grid.pathFind() → Spawn → Base arası puan ve yönleri (cell.targets) doldurur. updateEnemy() ilk çağrıda enemy.currentTarget dolu gelir, düşmanlar ilerlemeye başlar. Bu dört küçük ekleme/korumayla ekran görüntüsünde duran yaratıklar yol üzerinde akmaya başlayacak
User prompt
1 · Spawn & Başlangıç Yolu Kriter İstenilen Mevcut (td.txt) Yapılacak Spawn hücresi Izgaranın tam merkezi. Zaten merkezde (12,14). Korunacak. İlk yol Spawn’dan 1 kare AŞAĞI, sonra 3 kare SOL; son kare BASE. Sağ → Aşağı → 3 sol (fazladan bir “sağ” var). td İlk noktayı (sağ) sil; dizi [spawn, down, left×3] olarak yeniden tanımla. Yürünebilir alan Sadece “road” hücreleri (spawn, yol, base). Diğer tüm kareler duvardır/bloke. Type 0 “floor” şu an serbest geçiş; yol dışına çıkılabiliyor. Tüm hücreleri varsayılan duvar (type 1) yap; yalnız yol karelerini type 0/4 (passable) olarak ayarla. Grid path-finding artık sadece bu hücreleri geçerli saysın. 2 · Road Yerleştirme Mağazada “Road (10 G)” ikonu var (buton işlevi değil, kule ön-izleme mantığıyla sürükle-bırak). Yeni yol yalnızca mevcut bir yol karesinin Dört-yön (üst/alt/sol/sağ) komşusuna konabilir. Çapraz veya kopuk yerleştirme reddedilir. Yol satılamaz, yıkılamaz; bir kere kondu mu kalıcı. Yerleştirme onayı: Altın ≥ 10 → altın düş, yeni kare type=0/road olur. Altın yetersiz veya bağlantısız → kırmızı önizleme + uyarı bildirimi. Yeni yol eklenince spawn-dan base’e olan tekil yol güncellenir (istersen road dizisini sırayla tut, istersen yol hücrelerinden BFS ile dinamik rota çıkar). 3 · Enemy Davranışı Düşmanlar her zaman merkez spawnda doğar ve yalnızca road dizisini takip eder. Şu anki “entry-area” özel kodu (row < 4 iken düz in, sonra pathFind) tamamen kaldır. td Patikadan çıkmaları mümkün olmamalı; road biter bitmez base hücresine varırlar, Base’e çarptıklarında can azaltır, düşman yok olur. Uçan düşman varsa bile yine yol dışına çıkmasın (kanatlı da olsa yol TD mantığı). 4 · Path-Finding / Grid Mantığı Komple A* puanlı score alanı, 8-komşu path hesapları vs. gereksizleşti; Ya tamamen sil, ya da sadece road tipli hücrelerde çalışacak şekilde sadeleştir. Yol dizisi (veya road bağlantı grafı) spawn-dan base’e tek güzergâh sağlar; bloklu kontrol sadece “yol kopuk mu?” diye bakar. 5 · Kule & Mağaza Mağazada sadece Default (5 G), Rapid (15 G), Splash (25 G) + Road (10 G) kalacak. sniper, slow, poison sınıflarını ve tüm referanslarını temizle. TowerPreview/SourceTower kodu road için de kullan; road’da menzil halkası gösterme. 6 · UI & Bildirim Var olan hız göstergesi, skor, altın, can metinleri aynen korunacak. Road yerleştirince -10 G uçan yazı, yol karesi anında kahverengi tonuna döner. Yol yerleştirme uygun değilse “Bağlantısız yol!” veya “Yetersiz altın!” bildirimi göster. 7 · Silinecek / Temizlenecek Parçalar Otomatik “+Road” butonunun mevcut down fonksiyonu (yönü sabit uzatan) – artık yok. td entry area ve hasReachedEntryArea kontrolleri. Random duvar/şeffaf zemin üretimi (Grid init’te i==0 vs. j<=4 koşulları). Kullanılmayan kule tipleri ve onlara dair bullet/effect kodları. Yol kopukluğunu engellemek için yazılmış eski wouldBlockPath kontrolü – yeni bağlantı kuralı bu işlevi devralacak. 8 · Tamamlanma Kriterleri Düşmanlar merkezden çıkıp yalnız yol üzerinde ilerler, base’e vurunca hasar verir. Oyuncu yalnızca yol hücrelerinin 4-yön komşusuna yeni yol ekleyebilir; kopuk yol yerleşmez. Yol dışındaki tüm kareler gerçek engel; düşman asla basemize başka yoldan gidemez. Kod derlenir, konsol hatası vermez, debug katmanı (isteğe bağlı) yeni yol düzenini net gösterir. Bu listeyi uygula; nasıl kodlayacağını sen seç.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.damage = damage || 10; self.speed = speed || 5; self.x = startX; self.y = startY; var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Apply damage to target enemy self.targetEnemy.health -= self.damage; if (self.targetEnemy.health <= 0) { self.targetEnemy.health = 0; } else { self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70; } // Apply special effects based on bullet type if (self.type === 'splash') { // Create visual splash effect var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash'); game.addChild(splashEffect); // Splash damage to nearby enemies var splashRadius = CELL_SIZE * 1.5; for (var i = 0; i < enemies.length; i++) { var otherEnemy = enemies[i]; if (otherEnemy !== self.targetEnemy) { var splashDx = otherEnemy.x - self.targetEnemy.x; var splashDy = otherEnemy.y - self.targetEnemy.y; var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy); if (splashDistance <= splashRadius) { // Apply splash damage (50% of original damage) otherEnemy.health -= self.damage * 0.5; if (otherEnemy.health <= 0) { otherEnemy.health = 0; } else { otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70; } } } } } self.destroy(); } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } }; return self; }); var DebugCell = Container.expand(function () { var self = Container.call(this); var cellGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); cellGraphics.tint = Math.random() * 0xffffff; var debugArrows = []; var numberLabel = new Text2('0', { size: 30, fill: 0xFFFFFF, weight: 800 }); numberLabel.anchor.set(.5, .5); self.addChild(numberLabel); self.update = function () {}; self.down = function () { return; if (self.cell.type == 0 || self.cell.type == 1) { self.cell.type = self.cell.type == 1 ? 0 : 1; if (grid.pathFind()) { self.cell.type = self.cell.type == 1 ? 0 : 1; grid.pathFind(); var notification = game.addChild(new Notification("Path is blocked!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } grid.renderDebug(); } }; self.removeArrows = function () { while (debugArrows.length) { self.removeChild(debugArrows.pop()); } }; self.render = function (data) { switch (data.type) { case 0: { if (data.pathId != pathId) { self.removeArrows(); numberLabel.setText("-"); cellGraphics.tint = 0x880000; return; } numberLabel.visible = true; var tint = Math.floor(data.score / maxScore * 0x88); var towerInRangeHighlight = false; if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) { towerInRangeHighlight = true; cellGraphics.tint = 0x0088ff; } else { cellGraphics.tint = 0x88 - tint << 8 | tint; } while (debugArrows.length > data.targets.length) { self.removeChild(debugArrows.pop()); } for (var a = 0; a < data.targets.length; a++) { var destination = data.targets[a]; var ox = destination.x - data.x; var oy = destination.y - data.y; var angle = Math.atan2(oy, ox); if (!debugArrows[a]) { debugArrows[a] = LK.getAsset('arrow', { anchorX: -.5, anchorY: 0.5 }); debugArrows[a].alpha = .5; self.addChildAt(debugArrows[a], 1); } debugArrows[a].rotation = angle; } break; } case 2: { // spawn self.removeArrows(); cellGraphics.tint = 0x4444ff; // blue numberLabel.visible = false; break; } case 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } case 4: { // tower body self.removeArrows(); cellGraphics.tint = 0x555555; // dark gray numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); // This update method was incorrectly placed here and should be removed var EffectIndicator = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var effectGraphics = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); effectGraphics.blendMode = 1; switch (type) { case 'splash': effectGraphics.tint = 0x33CC00; effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5; break; case 'slow': effectGraphics.tint = 0x9900FF; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'poison': effectGraphics.tint = 0x00FFAA; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; case 'sniper': effectGraphics.tint = 0xFF5500; effectGraphics.width = effectGraphics.height = CELL_SIZE; break; } effectGraphics.alpha = 0.7; self.alpha = 0; // Animate the effect tween(self, { alpha: 0.8, scaleX: 1.5, scaleY: 1.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 2, scaleY: 2 }, { duration: 300, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); // Base enemy class for common functionality var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'normal'; self.speed = .01; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 100; self.health = self.maxHealth; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; // Check if this is a boss wave // Check if this is a boss wave // Apply different stats based on enemy type switch (self.type) { case 'fast': self.speed *= 2; // Twice as fast self.maxHealth = 100; break; case 'immune': self.isImmune = true; self.maxHealth = 80; break; case 'flying': self.isFlying = true; self.maxHealth = 80; break; case 'swarm': self.maxHealth = 50; // Weaker enemies break; case 'normal': default: // Normal enemy uses default values break; } if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') { self.isBoss = true; // Boss enemies have 20x health and are larger self.maxHealth *= 20; // Slower speed for bosses self.speed = self.speed * 0.7; } self.health = self.maxHealth; // Get appropriate asset for this enemy type var assetId = 'enemy'; if (self.type !== 'normal') { assetId = 'enemy_' + self.type; } var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); // Scale up boss enemies if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } // Fall back to regular enemy asset if specific type asset not found // Apply tint to differentiate enemy types /*switch (self.type) { case 'fast': enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies break; case 'immune': enemyGraphics.tint = 0xAA0000; // Red for immune enemies break; case 'flying': enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies break; case 'swarm': enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies break; }*/ // Create shadow for flying enemies if (self.isFlying) { // Create a shadow container that will be added to the shadow layer self.shadow = new Container(); // Clone the enemy graphics for the shadow var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', { anchorX: 0.5, anchorY: 0.5 }); // Apply shadow effect shadowGraphics.tint = 0x000000; // Black shadow shadowGraphics.alpha = 0.4; // Semi-transparent // If this is a boss, scale up the shadow to match if (self.isBoss) { shadowGraphics.scaleX = 1.8; shadowGraphics.scaleY = 1.8; } // Position shadow slightly offset self.shadow.x = 20; // Offset right self.shadow.y = 20; // Offset down // Ensure shadow has the same rotation as the enemy shadowGraphics.rotation = enemyGraphics.rotation; } var healthBarOutline = self.attachAsset('healthBarOutline', { anchorX: 0, anchorY: 0.5 }); var healthBarBG = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); var healthBar = self.attachAsset('healthBar', { anchorX: 0, anchorY: 0.5 }); healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10; healthBarOutline.x = -healthBarOutline.width / 2; healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5; healthBar.tint = 0x00ff00; healthBarBG.tint = 0xff0000; self.healthBar = healthBar; self.update = function () { if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; } // Handle slow effect if (self.isImmune) { // Immune enemies cannot be slowed or poisoned, clear any such effects self.slowed = false; self.slowEffect = false; self.poisoned = false; self.poisonEffect = false; // Reset speed to original if needed if (self.originalSpeed !== undefined) { self.speed = self.originalSpeed; } } else { // Handle slow effect if (self.slowed) { // Visual indication of slowed status if (!self.slowEffect) { self.slowEffect = true; } self.slowDuration--; if (self.slowDuration <= 0) { self.speed = self.originalSpeed; self.slowed = false; self.slowEffect = false; // Only reset tint if not poisoned if (!self.poisoned) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } // Handle poison effect if (self.poisoned) { // Visual indication of poisoned status if (!self.poisonEffect) { self.poisonEffect = true; } // Apply poison damage every 30 frames (twice per second) if (LK.ticks % 30 === 0) { self.health -= self.poisonDamage; if (self.health <= 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.poisoned = false; self.poisonEffect = false; // Only reset tint if not slowed if (!self.slowed) { enemyGraphics.tint = 0xFFFFFF; // Reset tint } } } } // Set tint based on effect status if (self.isImmune) { enemyGraphics.tint = 0xFFFFFF; } else if (self.poisoned && self.slowed) { // Combine poison (0x00FFAA) and slow (0x9900FF) colors // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212 enemyGraphics.tint = 0x4C7FD4; } else if (self.poisoned) { enemyGraphics.tint = 0x00FFAA; } else if (self.slowed) { enemyGraphics.tint = 0x9900FF; } else { enemyGraphics.tint = 0xFFFFFF; } if (self.currentTarget) { var ox = self.currentTarget.x - self.currentCellX; var oy = self.currentTarget.y - self.currentCellY; if (ox !== 0 || oy !== 0) { var angle = Math.atan2(oy, ox); if (enemyGraphics.targetRotation === undefined) { enemyGraphics.targetRotation = angle; enemyGraphics.rotation = angle; } else { if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) { tween.stop(enemyGraphics, { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemyGraphics.rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } enemyGraphics.targetRotation = angle; tween(enemyGraphics, { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; }; return self; }); var GoldIndicator = Container.expand(function (value, x, y) { var self = Container.call(this); var shadowText = new Text2("+" + value, { size: 45, fill: 0x000000, weight: 800 }); shadowText.anchor.set(0.5, 0.5); shadowText.x = 2; shadowText.y = 2; self.addChild(shadowText); var goldText = new Text2("+" + value, { size: 45, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); self.addChild(goldText); self.x = x; self.y = y; self.alpha = 0; self.scaleX = 0.5; self.scaleY = 0.5; tween(self, { alpha: 1, scaleX: 1.2, scaleY: 1.2, y: y - 40 }, { duration: 50, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, scaleX: 1.5, scaleY: 1.5, y: y - 80 }, { duration: 600, easing: tween.easeIn, delay: 800, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; self.spawns = []; self.goals = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { score: 0, pathId: 0, towersInRange: [] }; } } /* Cell Types 0: Transparent floor 1: Wall 2: Spawn 3: Goal */ for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; // Default all cells to walls var cellType = 1; cell.type = cellType; cell.x = i; cell.y = j; cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1]; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1]; cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left]; cell.targets = []; if (j > 3 && j <= gridHeight - 4) { var debugCell = new DebugCell(); self.addChild(debugCell); debugCell.cell = cell; debugCell.x = i * CELL_SIZE; debugCell.y = j * CELL_SIZE; cell.debugCell = debugCell; } } } // Set spawn at center (12, 14) var centerCell = self.cells[12] && self.cells[12][14]; if (centerCell) { centerCell.type = 2; self.spawns = [centerCell]; } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.pathFind = function () { var before = new Date().getTime(); var toProcess = self.goals.concat([]); maxScore = 0; pathId += 1; for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathId; } function processNode(node, targetValue, targetNode) { if (node && node.type != 1) { if (node.pathId < pathId || targetValue < node.score) { node.targets = [targetNode]; } else if (node.pathId == pathId && targetValue == node.score) { node.targets.push(targetNode); } if (node.pathId < pathId || targetValue < node.score) { node.score = targetValue; if (node.pathId != pathId) { toProcess.push(node); } node.pathId = pathId; if (targetValue > maxScore) { maxScore = targetValue; } } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score + 14142; if (node.up && node.left && node.up.type != 1 && node.left.type != 1) { processNode(node.upLeft, targetScore, node); } if (node.up && node.right && node.up.type != 1 && node.right.type != 1) { processNode(node.upRight, targetScore, node); } if (node.down && node.right && node.down.type != 1 && node.right.type != 1) { processNode(node.downRight, targetScore, node); } if (node.down && node.left && node.down.type != 1 && node.left.type != 1) { processNode(node.downLeft, targetScore, node); } targetScore = node.score + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } for (var a = 0; a < self.spawns.length; a++) { if (self.spawns[a].pathId != pathId) { console.warn("Spawn blocked"); return true; } else { // Limit spawn to only target the spawn exit (down direction) self.spawns[a].targets = [spawnExit]; } } for (var a = 0; a < enemies.length; a++) { var enemy = enemies[a]; // Skip flying enemies from path check as they can fly over obstacles if (enemy.isFlying) { continue; } var target = self.getCell(enemy.cellX, enemy.cellY); if (enemy.currentTarget) { if (enemy.currentTarget.pathId != pathId) { if (!target || target.pathId != pathId) { console.warn("Enemy blocked 1 "); return true; } } } else if (!target || target.pathId != pathId) { console.warn("Enemy blocked 2"); return true; } } console.log("Speed", new Date().getTime() - before); }; self.renderDebug = function () { for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var debugCell = self.cells[i][j].debugCell; if (debugCell) { debugCell.render(self.cells[i][j]); } } } }; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell.type == 3) { return true; } if (enemy.isFlying && enemy.shadow) { enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset // Match shadow rotation with enemy rotation if (enemy.children[0] && enemy.shadow.children[0]) { enemy.shadow.children[0].rotation = enemy.children[0].rotation; } } // Handle flying enemies differently if (enemy.isFlying) { // Flying enemies head straight to the closest goal if (!enemy.flyingTarget) { // Set flying target to the closest goal enemy.flyingTarget = self.goals[0]; // Find closest goal if there are multiple if (self.goals.length > 1) { var closestDist = Infinity; for (var i = 0; i < self.goals.length; i++) { var goal = self.goals[i]; var dx = goal.x - enemy.cellX; var dy = goal.y - enemy.cellY; var dist = dx * dx + dy * dy; if (dist < closestDist) { closestDist = dist; enemy.flyingTarget = goal; } } } } // Move directly toward the goal var ox = enemy.flyingTarget.x - enemy.currentCellX; var oy = enemy.flyingTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { // Reached the goal return true; } var angle = Math.atan2(oy, ox); // Rotate enemy graphic to match movement direction if (enemy.children[0] && enemy.children[0].targetRotation === undefined) { enemy.children[0].targetRotation = angle; enemy.children[0].rotation = angle; } else if (enemy.children[0]) { if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) { tween.stop(enemy.children[0], { rotation: true }); // Calculate the shortest angle to rotate var currentRotation = enemy.children[0].rotation; var angleDiff = angle - currentRotation; // Normalize angle difference to -PI to PI range for shortest path while (angleDiff > Math.PI) { angleDiff -= Math.PI * 2; } while (angleDiff < -Math.PI) { angleDiff += Math.PI * 2; } // Set target rotation and animate to it enemy.children[0].targetRotation = angle; tween(enemy.children[0], { rotation: currentRotation + angleDiff }, { duration: 250, easing: tween.easeOut }); } } // Update the cell position to track where the flying enemy is enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; // Update shadow position if this is a flying enemy return false; } // Handle normal pathfinding enemies using pathPoints array if (!enemy.currentTarget) { if (enemy.cellPathIndex === undefined) { enemy.cellPathIndex = 1; // Start with spawn exit } if (enemy.cellPathIndex < pathPoints.length) { enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } } if (enemy.currentTarget) { var ox = enemy.currentTarget.x - enemy.currentCellX; var oy = enemy.currentTarget.y - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.cellX = Math.round(enemy.currentCellX); enemy.cellY = Math.round(enemy.currentCellY); // Move to next point in path enemy.cellPathIndex++; if (enemy.cellPathIndex < pathPoints.length) { enemy.currentTarget = pathPoints[enemy.cellPathIndex]; } else { enemy.currentTarget = undefined; } return; } var angle = Math.atan2(oy, ox); enemy.currentCellX += Math.cos(angle) * enemy.speed; enemy.currentCellY += Math.sin(angle) * enemy.speed; } enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; }; }); var NextWaveButton = Container.expand(function () { var self = Container.call(this); var buttonBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 300; buttonBackground.height = 100; buttonBackground.tint = 0x0088FF; var buttonText = new Text2("Next Wave", { size: 50, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.enabled = false; self.visible = false; self.update = function () { if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) { self.enabled = true; self.visible = true; buttonBackground.tint = 0x0088FF; self.alpha = 1; } else { self.enabled = false; self.visible = false; buttonBackground.tint = 0x888888; self.alpha = 0.7; } }; self.down = function () { if (!self.enabled) { return; } if (waveIndicator.gameStarted && currentWave < totalWaves) { currentWave++; // Increment to the next wave directly waveTimer = 0; // Reset wave timer waveInProgress = true; waveSpawned = false; // Get the type of the current wave (which is now the next wave) var waveType = waveIndicator.getWaveTypeName(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); notificationGraphics.width = notificationText.width + 30; self.addChild(notificationText); self.alpha = 1; var fadeOutTime = 120; self.update = function () { if (fadeOutTime > 0) { fadeOutTime--; self.alpha = Math.min(fadeOutTime / 120 * 2, 1); } else { self.destroy(); } }; return self; }); var RoadPreview = Container.expand(function () { var self = Container.call(this); self.canPlace = false; self.gridX = 0; self.gridY = 0; // Create simple brown semi-transparent square using Graphics var roadGraphics = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); roadGraphics.tint = 0x8B4513; // Brown for road roadGraphics.alpha = 0.5; roadGraphics.width = CELL_SIZE * ROAD_SIZE; roadGraphics.height = CELL_SIZE * ROAD_SIZE; roadGraphics.pivot.set(roadGraphics.width / 2, roadGraphics.height / 2); // Add pulsing effect - start with null reference self.pulsingTween = null; self.startPulsing = function () { if (self.pulsingTween) return; self.pulsingTween = tween(roadGraphics, { alpha: 0.8 }, { duration: 500, easing: tween.easeInOut, loop: true, yoyo: true }); }; self.stopPulsing = function () { if (self.pulsingTween) { tween.stop(roadGraphics, { alpha: true }); self.pulsingTween = null; roadGraphics.alpha = 0.5; } }; // Start pulsing immediately self.startPulsing(); self.updatePlacementStatus = function () { self.canPlace = false; var canAfford = gold >= ROAD_COST; // Check if center cell is valid for road placement var centerCell = grid.getCell(self.gridX, self.gridY); // Road can only be placed on wall cells (type 1) without towers if (!centerCell || centerCell.type !== 1 || centerCell.hasTower) { // Can't place on non-wall cells or cells with towers self.canPlace = false; } else if (!canAfford) { // Not enough gold self.canPlace = false; } else { // Check if all cells in 3x3 block are valid var validBlock = true; for (var i = -1; i <= 1; i++) { for (var j = -1; j <= 1; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 1 || cell.hasTower) { validBlock = false; break; } } if (!validBlock) break; } if (!validBlock) { self.canPlace = false; } else { // Check if center cell is adjacent to spawn (forbidden) var centerX = self.gridX + 1; var centerY = self.gridY + 1; var touchingSpawn = Math.abs(centerX - spawn.x) + Math.abs(centerY - spawn.y) === 1; if (touchingSpawn) { self.canPlace = false; } else { // Check if center cell is adjacent to current base (end of current path) var base = pathPoints[pathPoints.length - 1]; var isAdjacentToBase = false; // Check 4-directional neighbors from base var baseNeighbors = [{ x: base.x, y: base.y - 1 }, // up { x: base.x + 1, y: base.y }, // right { x: base.x, y: base.y + 1 }, // down { x: base.x - 1, y: base.y } // left ]; // Check if center of 3x3 block (gridX+1, gridY+1) is adjacent to base var centerX = self.gridX + 1; var centerY = self.gridY + 1; for (var i = 0; i < baseNeighbors.length; i++) { if (baseNeighbors[i].x === centerX && baseNeighbors[i].y === centerY) { isAdjacentToBase = true; break; } } // Check if this would be going backwards var isBackwards = false; if (isAdjacentToBase && pathPoints.length >= 2) { var previousPoint = pathPoints[pathPoints.length - 2]; var currentDirection = { x: base.x - previousPoint.x, y: base.y - previousPoint.y }; var proposedDirection = { x: self.gridX - base.x, y: self.gridY - base.y }; // Check if proposed direction is opposite to current direction if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) { isBackwards = true; } } self.canPlace = isAdjacentToBase && !isBackwards; } } } // Update appearance - green for placeable, red for not placeable if (self.canPlace) { roadGraphics.tint = 0x00FF00; // Green for placeable self.visible = true; } else { roadGraphics.tint = 0xFF0000; // Red for not placeable self.visible = true; } }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; // Get center cell position, then adjust to get top-left of 3x3 block self.gridX = Math.floor(gridPosX / CELL_SIZE) - 1; self.gridY = Math.floor(gridPosY / CELL_SIZE) - 1; // Position at center of 3x3 block: (gridX + 1.5) * CELL_SIZE self.x = grid.x + (self.gridX + 1.5) * CELL_SIZE; self.y = grid.y + (self.gridY + 1.5) * CELL_SIZE; self.updatePlacementStatus(); }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; // Base graphics for source tower icon var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.scaleX = 1; baseGraphics.scaleY = 1; switch (self.towerType) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'splash': baseGraphics.tint = 0x33CC00; break; default: baseGraphics.tint = 0xAAAAAA; } var towerCost = getTowerCost(self.towerType); // Add shadow for tower type label var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); // Add tower type label var typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; // Position above center of tower self.addChild(typeLabel); // Add cost shadow var costLabelShadow = new Text2(towerCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); // Add cost label var costLabel = new Text2(towerCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Check if player can afford this tower var canAfford = gold >= getTowerCost(self.towerType); // Set opacity based on affordability self.alpha = canAfford ? 1 : 0.5; }; return self; }); var Tower = Container.expand(function (id) { var self = Container.call(this); self.id = id || 'default'; self.level = 1; self.maxLevel = 6; self.gridX = 0; self.gridY = 0; self.range = 3 * CELL_SIZE; // Standardized method to get the current range of the tower self.getRange = function () { // Always calculate range based on tower type and level switch (self.id) { case 'splash': // Splash: base 2, +0.2 per level (max ~4 blocks at max level) return (2 + (self.level - 1) * 0.2) * CELL_SIZE; case 'rapid': // Rapid: base 2.5, +0.5 per level return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE; default: // Default: base 3, +0.5 per level return (3 + (self.level - 1) * 0.5) * CELL_SIZE; } }; self.cellsInRange = []; self.fireRate = 60; self.bulletSpeed = 5; self.damage = 10; self.lastFired = 0; self.targetEnemy = null; switch (self.id) { case 'rapid': self.fireRate = 30; self.damage = 5; self.range = 2.5 * CELL_SIZE; self.bulletSpeed = 7; break; case 'splash': self.fireRate = 75; self.damage = 15; self.range = 2 * CELL_SIZE; self.bulletSpeed = 4; break; } var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.width = CELL_SIZE * TOWER_SIZE; baseGraphics.height = CELL_SIZE * TOWER_SIZE; switch (self.id) { case 'rapid': baseGraphics.tint = 0x00AAFF; break; case 'splash': baseGraphics.tint = 0x33CC00; break; default: baseGraphics.tint = 0xAAAAAA; } var levelIndicators = []; var maxDots = self.maxLevel; var dotSpacing = baseGraphics.width / (maxDots + 1); var dotSize = CELL_SIZE / 6; for (var i = 0; i < maxDots; i++) { var dot = new Container(); var outlineCircle = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); outlineCircle.width = dotSize + 4; outlineCircle.height = dotSize + 4; outlineCircle.tint = 0x000000; var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); towerLevelIndicator.width = dotSize; towerLevelIndicator.height = dotSize; towerLevelIndicator.tint = 0xCCCCCC; dot.x = -CELL_SIZE + dotSpacing * (i + 1); dot.y = CELL_SIZE * 0.7; self.addChild(dot); levelIndicators.push(dot); } var gunContainer = new Container(); self.addChild(gunContainer); var gunGraphics = gunContainer.attachAsset('defense', { anchorX: 0.5, anchorY: 0.5 }); self.updateLevelIndicators = function () { for (var i = 0; i < maxDots; i++) { var dot = levelIndicators[i]; var towerLevelIndicator = dot.children[1]; if (i < self.level) { towerLevelIndicator.tint = 0xFFFFFF; } else { switch (self.id) { case 'rapid': towerLevelIndicator.tint = 0x00AAFF; break; case 'splash': towerLevelIndicator.tint = 0x33CC00; break; default: towerLevelIndicator.tint = 0xAAAAAA; } } } }; self.updateLevelIndicators(); self.refreshCellsInRange = function () { for (var i = 0; i < self.cellsInRange.length; i++) { var cell = self.cellsInRange[i]; var towerIndex = cell.towersInRange.indexOf(self); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } self.cellsInRange = []; var rangeRadius = self.getRange() / CELL_SIZE; // Center of 3x3 block var centerX = self.gridX + 1.5; var centerY = self.gridY + 1.5; var minI = Math.floor(centerX - rangeRadius - 0.5); var maxI = Math.ceil(centerX + rangeRadius + 0.5); var minJ = Math.floor(centerY - rangeRadius - 0.5); var maxJ = Math.ceil(centerY + rangeRadius + 0.5); for (var i = minI; i <= maxI; i++) { for (var j = minJ; j <= maxJ; j++) { var closestX = Math.max(i, Math.min(centerX, i + 1)); var closestY = Math.max(j, Math.min(centerY, j + 1)); var deltaX = closestX - centerX; var deltaY = closestY - centerY; var distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared <= rangeRadius * rangeRadius) { var cell = grid.getCell(i, j); if (cell) { self.cellsInRange.push(cell); cell.towersInRange.push(self); } } } } grid.renderDebug(); }; self.getTotalValue = function () { var baseTowerCost = getTowerCost(self.id); var totalInvestment = baseTowerCost; var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost for (var i = 1; i < self.level; i++) { totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1)); } return totalInvestment; }; self.upgrade = function () { if (self.level < self.maxLevel) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.id); var upgradeCost; // Make last upgrade level extra expensive if (self.level === self.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1)); } if (gold >= upgradeCost) { setGold(gold - upgradeCost); self.level++; // No need to update self.range here; getRange() is now the source of truth // Apply tower-specific upgrades based on type if (self.id === 'rapid') { if (self.level === self.maxLevel) { // Extra powerful last upgrade (double the effect) self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect self.damage = 5 + self.level * 10; // double the effect self.bulletSpeed = 7 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades self.damage = 5 + self.level * 3; self.bulletSpeed = 7 + self.level * 0.7; } } else { if (self.level === self.maxLevel) { // Extra powerful last upgrade for all other towers (double the effect) self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect self.damage = 10 + self.level * 20; // double the effect self.bulletSpeed = 5 + self.level * 2.4; // double the effect } else { self.fireRate = Math.max(20, 60 - self.level * 8); self.damage = 10 + self.level * 5; self.bulletSpeed = 5 + self.level * 0.5; } } self.refreshCellsInRange(); self.updateLevelIndicators(); if (self.level > 1) { var levelDot = levelIndicators[self.level - 1].children[1]; tween(levelDot, { scaleX: 1.5, scaleY: 1.5 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { tween(levelDot, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeOut }); } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold to upgrade!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } return false; }; self.findTarget = function () { var closestEnemy = null; var closestScore = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is in range if (distance <= self.getRange()) { // Handle flying enemies differently - they can be targeted regardless of path if (enemy.isFlying) { // For flying enemies, prioritize by distance to the goal if (enemy.flyingTarget) { var goalX = enemy.flyingTarget.x; var goalY = enemy.flyingTarget.y; var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY)); // Use distance to goal as score if (distToGoal < closestScore) { closestScore = distToGoal; closestEnemy = enemy; } } else { // If no flying target yet (shouldn't happen), prioritize by distance to tower if (distance < closestScore) { closestScore = distance; closestEnemy = enemy; } } } else { // For ground enemies, use the original path-based targeting // Get the cell for this enemy var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.pathId === pathId) { // Use the cell's score (distance to exit) for prioritization // Lower score means closer to exit if (cell.score < closestScore) { closestScore = cell.score; closestEnemy = enemy; } } } } } if (!closestEnemy) { self.targetEnemy = null; } return closestEnemy; }; self.update = function () { self.targetEnemy = self.findTarget(); if (self.targetEnemy) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); gunContainer.rotation = angle; if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { var existingMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); var hasOwnMenu = false; var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self) { rangeCircle = game.children[i]; break; } } for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hasOwnMenu = true; break; } } if (hasOwnMenu) { for (var i = 0; i < existingMenus.length; i++) { if (existingMenus[i].tower === self) { hideUpgradeMenu(existingMenus[i]); } } if (rangeCircle) { game.removeChild(rangeCircle); } selectedTower = null; grid.renderDebug(); return; } for (var i = 0; i < existingMenus.length; i++) { existingMenus[i].destroy(); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = self; var rangeIndicator = new Container(); rangeIndicator.isTowerRange = true; rangeIndicator.tower = self; game.addChild(rangeIndicator); rangeIndicator.x = self.x; rangeIndicator.y = self.y; var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.getRange() * 2; rangeGraphics.alpha = 0.3; var upgradeMenu = new UpgradeMenu(self); game.addChild(upgradeMenu); upgradeMenu.x = 2048 / 2; tween(upgradeMenu, { y: 2732 - 225 }, { duration: 200, easing: tween.backOut }); grid.renderDebug(); }; self.isInRange = function (enemy) { if (!enemy) { return false; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); return distance <= self.getRange(); }; self.fire = function () { if (self.targetEnemy) { var potentialDamage = 0; for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) { potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage; } if (self.targetEnemy.health > potentialDamage) { var bulletX = self.x + Math.cos(gunContainer.rotation) * 40; var bulletY = self.y + Math.sin(gunContainer.rotation) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed); // Set bullet type based on tower type bullet.type = self.id; // Customize bullet appearance based on tower type switch (self.id) { case 'rapid': bullet.children[0].tint = 0x00AAFF; bullet.children[0].width = 20; bullet.children[0].height = 20; break; case 'splash': bullet.children[0].tint = 0x33CC00; bullet.children[0].width = 40; bullet.children[0].height = 40; break; } game.addChild(bullet); bullets.push(bullet); self.targetEnemy.bulletsTargetingThis.push(bullet); // --- Fire recoil effect for gunContainer --- // Stop any ongoing recoil tweens before starting a new one tween.stop(gunContainer, { x: true, y: true, scaleX: true, scaleY: true }); // Always use the original resting position for recoil, never accumulate offset if (gunContainer._restX === undefined) { gunContainer._restX = 0; } if (gunContainer._restY === undefined) { gunContainer._restY = 0; } if (gunContainer._restScaleX === undefined) { gunContainer._restScaleX = 1; } if (gunContainer._restScaleY === undefined) { gunContainer._restScaleY = 1; } // Reset to resting position before animating (in case of interrupted tweens) gunContainer.x = gunContainer._restX; gunContainer.y = gunContainer._restY; gunContainer.scaleX = gunContainer._restScaleX; gunContainer.scaleY = gunContainer._restScaleY; // Calculate recoil offset (recoil back along the gun's rotation) var recoilDistance = 8; var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance; var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance; // Animate recoil back from the resting position tween(gunContainer, { x: gunContainer._restX + recoilX, y: gunContainer._restY + recoilY }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { // Animate return to original position/scale tween(gunContainer, { x: gunContainer._restX, y: gunContainer._restY }, { duration: 90, easing: tween.cubicIn }); } }); } } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; // Position at center of 3x3 block: (gridX + 1.5) * CELL_SIZE self.x = grid.x + (gridX + 1.5) * CELL_SIZE; self.y = grid.y + (gridY + 1.5) * CELL_SIZE; for (var i = 0; i < TOWER_SIZE; i++) { for (var j = 0; j < TOWER_SIZE; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 4; // tower type cell.hasTower = true; // mark as occupied } } } self.refreshCellsInRange(); }; return self; }); var TowerPreview = Container.expand(function () { var self = Container.call(this); var towerRange = 3; var rangeInPixels = towerRange * CELL_SIZE; self.towerType = 'default'; self.hasEnoughGold = true; var rangeIndicator = new Container(); self.addChild(rangeIndicator); var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.alpha = 0.3; var previewGraphics = self.attachAsset('towerpreview', { anchorX: 0.5, anchorY: 0.5 }); previewGraphics.width = CELL_SIZE * TOWER_SIZE; previewGraphics.height = CELL_SIZE * TOWER_SIZE; previewGraphics.pivot.set(previewGraphics.width / 2, previewGraphics.height / 2); // Add pulsing effect to preview self.pulsingTween = null; self.startPulsing = function () { if (self.pulsingTween) return; self.pulsingTween = tween(previewGraphics, { alpha: 0.8 }, { duration: 500, easing: tween.easeInOut, loop: true, yoyo: true }); }; self.stopPulsing = function () { if (self.pulsingTween) { tween.stop(previewGraphics, { alpha: true }); self.pulsingTween = null; previewGraphics.alpha = 0.5; } }; self.canPlace = false; self.gridX = 0; self.gridY = 0; self.blockedByEnemy = false; self.update = function () { var previousHasEnoughGold = self.hasEnoughGold; self.hasEnoughGold = gold >= getTowerCost(self.towerType); // Only update appearance if the affordability status has changed if (previousHasEnoughGold !== self.hasEnoughGold) { self.updateAppearance(); } }; self.updateAppearance = function () { // Use Tower class to get the source of truth for range var tempTower = new Tower(self.towerType); var previewRange = tempTower.getRange(); // Clean up tempTower to avoid memory leaks if (tempTower && tempTower.destroy) { tempTower.destroy(); } // Set range indicator using unified range logic rangeGraphics.width = rangeGraphics.height = previewRange * 2; switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'splash': previewGraphics.tint = 0x33CC00; break; default: previewGraphics.tint = 0xAAAAAA; } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } else { // Green tint for valid placement switch (self.towerType) { case 'rapid': previewGraphics.tint = 0x00AAFF; break; case 'splash': previewGraphics.tint = 0x33CC00; break; default: previewGraphics.tint = 0xAAAAAA; } } }; self.updatePlacementStatus = function () { var validGridPlacement = true; if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) { validGridPlacement = false; } else { for (var i = 0; i < TOWER_SIZE; i++) { for (var j = 0; j < TOWER_SIZE; j++) { var cell = grid.getCell(self.gridX + i, self.gridY + j); // Towers can only be placed on wall cells (type 1) and must not have towers if (!cell || cell.type !== 1 || cell.hasTower) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } self.blockedByEnemy = false; if (validGridPlacement) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.currentCellY < 4) { continue; } // Only check non-flying enemies, flying enemies can pass over towers if (!enemy.isFlying) { if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + TOWER_SIZE && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + TOWER_SIZE) { self.blockedByEnemy = true; break; } if (enemy.currentTarget) { var targetX = enemy.currentTarget.x; var targetY = enemy.currentTarget.y; if (targetX >= self.gridX && targetX < self.gridX + TOWER_SIZE && targetY >= self.gridY && targetY < self.gridY + TOWER_SIZE) { self.blockedByEnemy = true; break; } } } } } // Check if tower touches a road var touchesRoad = false; if (validGridPlacement) { var dirs = [[0, -1], [1, 0], [0, 1], [-1, 0]]; // 4 directions for (var tx = 0; tx < TOWER_SIZE; tx++) { for (var ty = 0; ty < TOWER_SIZE; ty++) { for (var d = 0; d < dirs.length; d++) { var nx = self.gridX + tx + dirs[d][0]; var ny = self.gridY + ty + dirs[d][1]; var nCell = grid.getCell(nx, ny); if (nCell && (nCell.type === 0 || nCell.type === 2 || nCell.type === 3)) { touchesRoad = true; break; } } if (touchesRoad) break; } if (touchesRoad) break; } } self.canPlace = validGridPlacement && touchesRoad && !self.blockedByEnemy; self.hasEnoughGold = gold >= getTowerCost(self.towerType); self.updateAppearance(); }; self.checkPlacement = function () { self.updatePlacementStatus(); }; self.snapToGrid = function (x, y) { var gridPosX = x - grid.x; var gridPosY = y - grid.y; // Get center cell position, then adjust to get top-left of 3x3 block self.gridX = Math.floor(gridPosX / CELL_SIZE) - 1; self.gridY = Math.floor(gridPosY / CELL_SIZE) - 1; // Position at center of 3x3 block: (gridX + 1.5) * CELL_SIZE self.x = grid.x + (self.gridX + 1.5) * CELL_SIZE; self.y = grid.y + (self.gridY + 1.5) * CELL_SIZE; self.checkPlacement(); }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; menuBackground.tint = 0x444444; menuBackground.alpha = 0.9; var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var buttonsContainer = new Container(); buttonsContainer.x = 500; self.addChild(buttonsContainer); var upgradeButton = new Container(); buttonsContainer.addChild(upgradeButton); var buttonBackground = upgradeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); buttonBackground.width = 500; buttonBackground.height = 150; var isMaxLevel = self.tower.level >= self.tower.maxLevel; // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var upgradeCost; if (isMaxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888; var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); buttonText.anchor.set(0.5, 0.5); upgradeButton.addChild(buttonText); var sellButton = new Container(); buttonsContainer.addChild(sellButton); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); sellButtonBackground.width = 500; sellButtonBackground.height = 150; sellButtonBackground.tint = 0xCC0000; var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', { size: 60, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); upgradeButton.y = -85; sellButton.y = 85; var closeButton = new Container(); self.addChild(closeButton); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); closeBackground.width = 90; closeBackground.height = 90; closeBackground.tint = 0xAA0000; var closeText = new Text2('X', { size: 68, fill: 0xFFFFFF, weight: 800 }); closeText.anchor.set(0.5, 0.5); closeButton.addChild(closeText); closeButton.x = menuBackground.width / 2 - 57; closeButton.y = -menuBackground.height / 2 + 57; upgradeButton.down = function (x, y, obj) { if (self.tower.level >= self.tower.maxLevel) { var notification = game.addChild(new Notification("Tower is already at max level!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (self.tower.upgrade()) { // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); if (self.tower.level >= self.tower.maxLevel) { upgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s'); buttonText.setText('Upgrade: ' + upgradeCost + ' gold'); var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = Math.floor(totalInvestment * 0.6); sellButtonText.setText('Sell: +' + sellValue + ' gold'); if (self.tower.level >= self.tower.maxLevel) { buttonBackground.tint = 0x888888; buttonText.setText('Max Level'); } var rangeCircle = null; for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { rangeCircle = game.children[i]; break; } } if (rangeCircle) { var rangeGraphics = rangeCircle.children[0]; rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; } else { var newRangeIndicator = new Container(); newRangeIndicator.isTowerRange = true; newRangeIndicator.tower = self.tower; game.addChildAt(newRangeIndicator, 0); newRangeIndicator.x = self.tower.x; newRangeIndicator.y = self.tower.y; var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2; rangeGraphics.alpha = 0.3; } tween(self, { scaleX: 1.05, scaleY: 1.05 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeIn }); } }); } }; sellButton.down = function (x, y, obj) { var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0; var sellValue = getTowerSellValue(totalInvestment); setGold(gold + sellValue); var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < TOWER_SIZE; i++) { for (var j = 0; j < TOWER_SIZE; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; // reset to wall cell.hasTower = false; // mark as free var towerIndex = cell.towersInRange.indexOf(self.tower); if (towerIndex !== -1) { cell.towersInRange.splice(towerIndex, 1); } } } } if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); grid.pathFind(); grid.renderDebug(); self.destroy(); for (var i = 0; i < game.children.length; i++) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); break; } } }; closeButton.down = function (x, y, obj) { hideUpgradeMenu(self); selectedTower = null; grid.renderDebug(); }; self.update = function () { if (self.tower.level >= self.tower.maxLevel) { if (buttonText.text !== 'Max Level') { buttonText.setText('Max Level'); buttonBackground.tint = 0x888888; } return; } // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost var baseUpgradeCost = getTowerCost(self.tower.id); var currentUpgradeCost; if (self.tower.level >= self.tower.maxLevel) { currentUpgradeCost = 0; } else if (self.tower.level === self.tower.maxLevel - 1) { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2); } else { currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1)); } var canAfford = gold >= currentUpgradeCost; buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888; var newText = 'Upgrade: ' + currentUpgradeCost + ' gold'; if (buttonText.text !== newText) { buttonText.setText(newText); } }; return self; }); var WaveIndicator = Container.expand(function () { var self = Container.call(this); self.gameStarted = false; self.waveMarkers = []; self.waveTypes = []; self.enemyCounts = []; self.indicatorWidth = 0; self.lastBossType = null; // Track the last boss type to avoid repeating var blockWidth = 400; var totalBlocksWidth = blockWidth * totalWaves; var startMarker = new Container(); var startBlock = startMarker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); startBlock.width = blockWidth - 10; startBlock.height = 70 * 2; startBlock.tint = 0x00AA00; // Add shadow for start text var startTextShadow = new Text2("Start Game", { size: 50, fill: 0x000000, weight: 800 }); startTextShadow.anchor.set(0.5, 0.5); startTextShadow.x = 4; startTextShadow.y = 4; startMarker.addChild(startTextShadow); var startText = new Text2("Start Game", { size: 50, fill: 0xFFFFFF, weight: 800 }); startText.anchor.set(0.5, 0.5); startMarker.addChild(startText); startMarker.x = -self.indicatorWidth; self.addChild(startMarker); self.waveMarkers.push(startMarker); startMarker.down = function () { if (!self.gameStarted) { self.gameStarted = true; currentWave = 0; waveTimer = nextWaveTime; startBlock.tint = 0x00FF00; startText.setText("Started!"); startTextShadow.setText("Started!"); // Make sure shadow position remains correct after text change startTextShadow.x = 4; startTextShadow.y = 4; var notification = game.addChild(new Notification("Game started! Wave 1 incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } }; for (var i = 0; i < totalWaves; i++) { var marker = new Container(); var block = marker.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); block.width = blockWidth - 10; block.height = 70 * 2; // --- Begin new unified wave logic --- var waveType = "normal"; var enemyType = "normal"; var enemyCount = 10; var isBossWave = (i + 1) % 10 === 0; // Ensure all types appear in early waves if (i === 0) { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } else if (i === 1) { block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if (i === 2) { block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if (i === 3) { block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if (i === 4) { block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else if (isBossWave) { // Boss waves: cycle through all boss types, last boss is always flying var bossTypes = ['normal', 'fast', 'immune', 'flying']; var bossTypeIndex = Math.floor((i + 1) / 10) - 1; if (i === totalWaves - 1) { // Last boss is always flying enemyType = 'flying'; waveType = "Boss Flying"; block.tint = 0xFFFF00; } else { enemyType = bossTypes[bossTypeIndex % bossTypes.length]; switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; waveType = "Boss Normal"; break; case 'fast': block.tint = 0x00AAFF; waveType = "Boss Fast"; break; case 'immune': block.tint = 0xAA0000; waveType = "Boss Immune"; break; case 'flying': block.tint = 0xFFFF00; waveType = "Boss Flying"; break; } } enemyCount = 1; // Make the wave indicator for boss waves stand out // Set boss wave color to the color of the wave type switch (enemyType) { case 'normal': block.tint = 0xAAAAAA; break; case 'fast': block.tint = 0x00AAFF; break; case 'immune': block.tint = 0xAA0000; break; case 'flying': block.tint = 0xFFFF00; break; default: block.tint = 0xFF0000; break; } } else if ((i + 1) % 5 === 0) { // Every 5th non-boss wave is fast block.tint = 0x00AAFF; waveType = "Fast"; enemyType = "fast"; enemyCount = 10; } else if ((i + 1) % 4 === 0) { // Every 4th non-boss wave is immune block.tint = 0xAA0000; waveType = "Immune"; enemyType = "immune"; enemyCount = 10; } else if ((i + 1) % 7 === 0) { // Every 7th non-boss wave is flying block.tint = 0xFFFF00; waveType = "Flying"; enemyType = "flying"; enemyCount = 10; } else if ((i + 1) % 3 === 0) { // Every 3rd non-boss wave is swarm block.tint = 0xFF00FF; waveType = "Swarm"; enemyType = "swarm"; enemyCount = 30; } else { block.tint = 0xAAAAAA; waveType = "Normal"; enemyType = "normal"; enemyCount = 10; } // --- End new unified wave logic --- // Mark boss waves with a special visual indicator if (isBossWave && enemyType !== 'swarm') { // Add a crown or some indicator to the wave marker for boss waves var bossIndicator = marker.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); bossIndicator.width = 30; bossIndicator.height = 30; bossIndicator.tint = 0xFFD700; // Gold color bossIndicator.y = -block.height / 2 - 15; // Change the wave type text to indicate boss waveType = "BOSS"; } // Store the wave type and enemy count self.waveTypes[i] = enemyType; self.enemyCounts[i] = enemyCount; // Add shadow for wave type - 30% smaller than before var waveTypeShadow = new Text2(waveType, { size: 56, fill: 0x000000, weight: 800 }); waveTypeShadow.anchor.set(0.5, 0.5); waveTypeShadow.x = 4; waveTypeShadow.y = 4; marker.addChild(waveTypeShadow); // Add wave type text - 30% smaller than before var waveTypeText = new Text2(waveType, { size: 56, fill: 0xFFFFFF, weight: 800 }); waveTypeText.anchor.set(0.5, 0.5); waveTypeText.y = 0; marker.addChild(waveTypeText); // Add shadow for wave number - 20% larger than before var waveNumShadow = new Text2((i + 1).toString(), { size: 48, fill: 0x000000, weight: 800 }); waveNumShadow.anchor.set(1.0, 1.0); waveNumShadow.x = blockWidth / 2 - 16 + 5; waveNumShadow.y = block.height / 2 - 12 + 5; marker.addChild(waveNumShadow); // Main wave number text - 20% larger than before var waveNum = new Text2((i + 1).toString(), { size: 48, fill: 0xFFFFFF, weight: 800 }); waveNum.anchor.set(1.0, 1.0); waveNum.x = blockWidth / 2 - 16; waveNum.y = block.height / 2 - 12; marker.addChild(waveNum); marker.x = -self.indicatorWidth + (i + 1) * blockWidth; self.addChild(marker); self.waveMarkers.push(marker); } // Get wave type for a specific wave number self.getWaveType = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return "normal"; } // If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType // then we should return a different boss type var waveType = self.waveTypes[waveNumber - 1]; return waveType; }; // Get enemy count for a specific wave number self.getEnemyCount = function (waveNumber) { if (waveNumber < 1 || waveNumber > totalWaves) { return 10; } return self.enemyCounts[waveNumber - 1]; }; // Get display name for a wave type self.getWaveTypeName = function (waveNumber) { var type = self.getWaveType(waveNumber); var typeName = type.charAt(0).toUpperCase() + type.slice(1); // Add boss prefix for boss waves (every 10th wave) if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') { typeName = "BOSS"; } return typeName; }; self.positionIndicator = new Container(); var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator.width = blockWidth - 10; indicator.height = 16; indicator.tint = 0xffad0e; indicator.y = -65; var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); indicator2.width = blockWidth - 10; indicator2.height = 16; indicator2.tint = 0xffad0e; indicator2.y = 65; var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); leftWall.width = 16; leftWall.height = 146; leftWall.tint = 0xffad0e; leftWall.x = -(blockWidth - 16) / 2; var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', { anchorX: 0.5, anchorY: 0.5 }); rightWall.width = 16; rightWall.height = 146; rightWall.tint = 0xffad0e; rightWall.x = (blockWidth - 16) / 2; self.addChild(self.positionIndicator); self.update = function () { var progress = waveTimer / nextWaveTime; var moveAmount = (progress + currentWave) * blockWidth; for (var i = 0; i < self.waveMarkers.length; i++) { var marker = self.waveMarkers[i]; marker.x = -moveAmount + i * blockWidth; } self.positionIndicator.x = 0; for (var i = 0; i < totalWaves + 1; i++) { var marker = self.waveMarkers[i]; if (i === 0) { continue; } var block = marker.children[0]; if (i - 1 < currentWave) { block.alpha = .5; } } self.handleWaveProgression = function () { if (!self.gameStarted) { return; } if (currentWave < totalWaves) { waveTimer += gameSpeed; if (waveTimer >= nextWaveTime) { waveTimer = 0; currentWave++; waveInProgress = true; waveSpawned = false; if (currentWave != 1) { var waveType = self.getWaveTypeName(currentWave); var enemyCount = self.getEnemyCount(currentWave); var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } } } }; self.handleWaveProgression(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ // Define CELL_SIZE early so it can be used in asset initialization var CELL_SIZE = 76; var isHidingUpgradeMenu = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; } }); } var ROAD_SIZE = 3; // 3x3 blocks for roads var TOWER_SIZE = 3; // 3x3 blocks for towers var ROAD_COST = 5; var pathId = 1; var maxScore = 0; var enemies = []; var towers = []; var bullets = []; var defenses = []; var selectedTower = null; var gold = 80; var lives = 20; var score = 0; var currentWave = 0; var totalWaves = 50; var waveTimer = 0; var waveInProgress = false; var waveSpawned = false; var nextWaveTime = 12000 / 2; var sourceTower = null; var enemiesToSpawn = 10; // Default number of enemies per wave // Game speed control var gameSpeed = 1; var gameSpeedFactors = [1, 3, 5, 0.5, 0]; var currentSpeedIndex = 0; var goldText = new Text2('Gold: ' + gold, { size: 60, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); var livesText = new Text2('Lives: ' + lives, { size: 60, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); var scoreText = new Text2('Score: ' + score, { size: 60, fill: 0xFF0000, weight: 800 }); scoreText.anchor.set(0.5, 0.5); var topMargin = 50; var centerX = 2048 / 2; var spacing = 400; LK.gui.top.addChild(goldText); LK.gui.top.addChild(livesText); LK.gui.top.addChild(scoreText); livesText.x = 0; livesText.y = topMargin; goldText.x = -spacing; goldText.y = topMargin; scoreText.x = spacing; scoreText.y = topMargin; var speedText = new Text2('1x', { size: 60, fill: 0xFFFFFF, weight: 800 }); speedText.anchor.set(0.5, 0.5); speedText.x = spacing + 30; speedText.y = topMargin; LK.gui.top.addChild(speedText); speedText.down = function () { currentSpeedIndex = (currentSpeedIndex + 1) % gameSpeedFactors.length; gameSpeed = gameSpeedFactors[currentSpeedIndex]; if (gameSpeed === 0) { speedText.setText('STOP'); } else { speedText.setText(gameSpeed + 'x'); } }; function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); scoreText.setText('Score: ' + score); } function setGold(value) { gold = value; updateUI(); } var debugLayer = new Container(); var towerLayer = new Container(); // Create three separate layers for enemy hierarchy var enemyLayerBottom = new Container(); // For normal enemies var enemyLayerMiddle = new Container(); // For shadows var enemyLayerTop = new Container(); // For flying enemies var enemyLayer = new Container(); // Main container to hold all enemy layers // Add layers in correct order (bottom first, then middle for shadows, then top) enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); var grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; // Initialize the dynamic path function buildPath(spawnX, spawnY) { var pts = []; pts.push({ x: spawnX, y: spawnY }); // spawn pts.push({ x: spawnX, y: spawnY + 1 }); // 1 down (3x3 block center) for (var k = 1; k <= 3; k++) { // 3 left (3x3 block centers) pts.push({ x: spawnX - k, y: spawnY + 1 }); } return pts; // last element is base } var pathPoints = buildPath(12, 14); // Define spawn and spawn exit for enemy pathfinding var spawn = grid.getCell(12, 14); var spawnExit = grid.getCell(12, 15); // Mark path cells in grid with 3x3 blocks for (var i = 0; i < pathPoints.length; i++) { var point = pathPoints[i]; var centerCell = grid.getCell(point.x, point.y); if (centerCell) { // Create 3x3 block around center for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var cell = grid.getCell(point.x + dx, point.y + dy); if (cell) { if (i === 0) { cell.type = 2; // spawn } else if (i === pathPoints.length - 1) { cell.type = 3; // goal if (dx === 0 && dy === 0) { grid.goals.push(cell); // Add center of base to goals array } } else { cell.type = 0; // path } } } } } } grid.pathFind(); grid.renderDebug(); debugLayer.addChild(grid); game.addChild(debugLayer); game.addChild(towerLayer); game.addChild(enemyLayer); var offset = 0; var towerPreview = new TowerPreview(); game.addChild(towerPreview); towerPreview.visible = false; var roadPreview = new RoadPreview(); game.addChild(roadPreview); roadPreview.visible = false; var isDragging = false; var dragType = null; // 'tower' or 'road' function wouldBlockPath(gridX, gridY) { // Always return false as enemies can only walk on road tiles return false; } function getTowerCost(towerType) { switch (towerType) { case 'rapid': return 15; case 'splash': return 25; default: return 5; } } function getTowerSellValue(totalValue) { return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue; } function placeTower(gridX, gridY, towerType) { var towerCost = getTowerCost(towerType); if (gold >= towerCost) { var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); grid.pathFind(); grid.renderDebug(); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } game.down = function (x, y, obj) { var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenuVisible) { return; } // Check if clicking on road source if (x >= roadSource.x - roadSource.width / 2 && x <= roadSource.x + roadSource.width / 2 && y >= roadSource.y - roadSource.height / 2 && y <= roadSource.y + roadSource.height / 2) { roadPreview.visible = true; isDragging = true; dragType = 'road'; roadPreview.snapToGrid(x, y); return; } // Check tower sources for (var i = 0; i < sourceTowers.length; i++) { var tower = sourceTowers[i]; if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) { towerPreview.visible = true; isDragging = true; dragType = 'tower'; towerPreview.towerType = tower.towerType; towerPreview.updateAppearance(); towerPreview.startPulsing(); // Apply the same offset as in move handler to ensure consistency when starting drag towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); break; } } }; game.move = function (x, y, obj) { if (isDragging) { // Shift the y position upward by 1.5 tiles to show preview above finger if (dragType === 'road') { roadPreview.snapToGrid(x, y); } else { towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5); } } }; game.up = function (x, y, obj) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerLeft = tower.x - tower.width / 2; var towerRight = tower.x + tower.width / 2; var towerTop = tower.y - tower.height / 2; var towerBottom = tower.y + tower.height / 2; if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) { clickedOnTower = true; break; } } var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) { var clickedOnMenu = false; for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuWidth = 2048; var menuHeight = 450; var menuLeft = menu.x - menuWidth / 2; var menuRight = menu.x + menuWidth / 2; var menuTop = menu.y - menuHeight / 2; var menuBottom = menu.y + menuHeight / 2; if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) { clickedOnMenu = true; break; } } if (!clickedOnMenu) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; hideUpgradeMenu(menu); } for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange) { game.removeChild(game.children[i]); } } selectedTower = null; grid.renderDebug(); } } if (isDragging) { isDragging = false; if (dragType === 'road') { // Handle road placement if (roadPreview.canPlace && gold >= ROAD_COST) { // Get center cell of 3x3 block var centerX = roadPreview.gridX + 1; var centerY = roadPreview.gridY + 1; var centerCell = grid.getCell(centerX, centerY); if (centerCell) { // 2-1) Deduct gold setGold(gold - ROAD_COST); // 2-2) Convert old base block to normal road var oldBase = grid.goals[0]; // Convert old base and surrounding cells to road for (var i = -1; i <= 1; i++) { for (var j = -1; j <= 1; j++) { var oldCell = grid.getCell(oldBase.x + i, oldBase.y + j); if (oldCell) { oldCell.type = 0; // convert to road oldCell.hasTower = false; } } } // 2-3) Make new center cell and surrounding cells the base for (var i = -1; i <= 1; i++) { for (var j = -1; j <= 1; j++) { var newCell = grid.getCell(centerX + i, centerY + j); if (newCell) { newCell.type = 3; // make this the new base newCell.hasTower = false; } } } grid.goals[0] = centerCell; // update goals array to center // 2-4) Update pathPoints array for debug arrows pathPoints.push({ x: centerX, y: centerY }); // 2-5) Recalculate path and redraw grid.pathFind(); grid.renderDebug(); // Show gold deduction var goldIndicator = new GoldIndicator(-ROAD_COST, roadPreview.x, roadPreview.y); game.addChild(goldIndicator); } } else if (gold < ROAD_COST) { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { // Check specific reasons for failure var base = pathPoints[pathPoints.length - 1]; var isAdjacentToBase = false; var baseNeighbors = [{ x: base.x, y: base.y - 1 }, // up { x: base.x + 1, y: base.y }, // right { x: base.x, y: base.y + 1 }, // down { x: base.x - 1, y: base.y } // left ]; for (var i = 0; i < baseNeighbors.length; i++) { if (baseNeighbors[i].x === roadPreview.gridX && baseNeighbors[i].y === roadPreview.gridY) { isAdjacentToBase = true; break; } } if (!isAdjacentToBase) { var notification = game.addChild(new Notification("Road must connect to Base (front/sides)!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else { var notification = game.addChild(new Notification("Road can't go backwards!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } roadPreview.visible = false; } else { // Handle tower placement if (towerPreview.canPlace) { if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) { placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType); } else { var notification = game.addChild(new Notification("Tower would block the path!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else if (towerPreview.blockedByEnemy) { var notification = game.addChild(new Notification("Cannot build: Enemy in the way!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } else if (towerPreview.visible) { var notification = game.addChild(new Notification("Cannot build here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } towerPreview.visible = false; towerPreview.stopPulsing(); } dragType = null; if (isDragging) { var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); for (var i = 0; i < upgradeMenus.length; i++) { upgradeMenus[i].destroy(); } } } }; var waveIndicator = new WaveIndicator(); waveIndicator.x = 2048 / 2; waveIndicator.y = 2732 - 80; game.addChild(waveIndicator); var nextWaveButtonContainer = new Container(); var nextWaveButton = new NextWaveButton(); nextWaveButton.x = 2048 - 200; nextWaveButton.y = 2732 - 100 + 20; nextWaveButtonContainer.addChild(nextWaveButton); game.addChild(nextWaveButtonContainer); var buildItems = ['default', 'rapid', 'splash']; var sourceTowers = []; var towerSpacing = 300; // Increase spacing for larger towers // Account for road button in spacing calculation var totalItems = buildItems.length + 1; // +1 for road var startX = 2048 / 2 - totalItems * towerSpacing / 2 + towerSpacing / 2; var towerY = 2732 - CELL_SIZE * 3 - 90; // Create road source button var roadSource = new Container(); roadSource.interactive = true; roadSource.buttonMode = true; roadSource.data = { type: "road" }; var roadBg = roadSource.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); roadBg.scaleX = 1; roadBg.scaleY = 1; roadBg.tint = 0x8B4513; // Brown color for road var roadLabel = new Text2('Road\n' + ROAD_COST, { size: 50, fill: 0xFFFFFF, weight: 800 }); roadLabel.anchor.set(0.5, 0.5); roadLabel.y = 0; roadSource.addChild(roadLabel); roadSource.x = startX; roadSource.y = towerY; roadSource.update = function () { var canAfford = gold >= ROAD_COST; roadSource.alpha = canAfford ? 1 : 0.5; }; towerLayer.addChild(roadSource); // Create tower sources (offset by 1 to account for road) for (var i = 0; i < buildItems.length; i++) { var tower = new SourceTower(buildItems[i]); tower.x = startX + (i + 1) * towerSpacing; tower.y = towerY; towerLayer.addChild(tower); sourceTowers.push(tower); } sourceTower = null; enemiesToSpawn = 10; game.update = function () { // Apply game speed (skip updates if paused) if (gameSpeed === 0) { return; // Game is paused } // Scale time-based updates by game speed var speedMultiplier = gameSpeed; if (waveInProgress) { if (!waveSpawned) { waveSpawned = true; // Get wave type and enemy count from the wave indicator var waveType = waveIndicator.getWaveType(currentWave); var enemyCount = waveIndicator.getEnemyCount(currentWave); // Check if this is a boss wave var isBossWave = currentWave % 10 === 0 && currentWave > 0; if (isBossWave && waveType !== 'swarm') { // Boss waves have just 1 enemy regardless of what the wave indicator says enemyCount = 1; // Show boss announcement var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️")); notification.x = 2048 / 2; notification.y = grid.height - 200; } // Spawn the appropriate number of enemies for (var i = 0; i < enemyCount; i++) { var enemy = new Enemy(waveType); // Add enemy to the appropriate layer based on type if (enemy.isFlying) { // Add flying enemy to the top layer enemyLayerTop.addChild(enemy); // If it's a flying enemy, add its shadow to the middle layer if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { // Add normal/ground enemies to the bottom layer enemyLayerBottom.addChild(enemy); } // Scale difficulty with wave number but don't apply to boss // as bosses already have their health multiplier // Use exponential scaling for health var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; // Increment speed slightly with wave number //enemy.speed = enemy.speed + currentWave * 0.002; // Spawn at the defined spawn point (center of grid) var spawnPoint = pathPoints[0]; enemy.cellX = spawnPoint.x; enemy.cellY = spawnPoint.y; enemy.currentCellX = spawnPoint.x; enemy.currentCellY = spawnPoint.y; enemy.x = grid.x + spawnPoint.x * CELL_SIZE + CELL_SIZE / 2; enemy.y = grid.y + spawnPoint.y * CELL_SIZE + CELL_SIZE / 2; enemy.waveNumber = currentWave; // Initialize enemy pathfinding - start with spawn exit as first target enemy.currentTarget = spawnExit; enemy.cellPathIndex = 1; // Index into pathPoints array enemies.push(enemy); } } var currentWaveEnemiesRemaining = false; for (var i = 0; i < enemies.length; i++) { if (enemies[i].waveNumber === currentWave) { currentWaveEnemiesRemaining = true; break; } } if (waveSpawned && !currentWaveEnemiesRemaining) { waveInProgress = false; waveSpawned = false; } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) { var bullet = enemy.bulletsTargetingThis[i]; bullet.targetEnemy = null; } // Boss enemies give more gold and score var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5); var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y); game.addChild(goldIndicator); setGold(gold + goldEarned); // Give more score for defeating a boss var scoreValue = enemy.isBoss ? 100 : 5; score += scoreValue; // Add a notification for boss defeat if (enemy.isBoss) { var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!")); notification.x = 2048 / 2; notification.y = grid.height - 150; } updateUI(); // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); continue; } if (grid.updateEnemy(enemy)) { // Clean up shadow if it's a flying enemy if (enemy.isFlying && enemy.shadow) { enemyLayerMiddle.removeChild(enemy.shadow); enemy.shadow = null; } // Remove enemy from the appropriate layer if (enemy.isFlying) { enemyLayerTop.removeChild(enemy); } else { enemyLayerBottom.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } for (var i = bullets.length - 1; i >= 0; i--) { if (!bullets[i].parent) { if (bullets[i].targetEnemy) { var targetEnemy = bullets[i].targetEnemy; var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]); if (bulletIndex !== -1) { targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1); } } bullets.splice(i, 1); } } if (towerPreview.visible) { towerPreview.checkPlacement(); } if (roadPreview.visible) { roadPreview.updatePlacementStatus(); } if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) { LK.showYouWin(); } };
===================================================================
--- original.js
+++ change.js
@@ -918,9 +918,11 @@
if (!validBlock) {
self.canPlace = false;
} else {
// Check if center cell is adjacent to spawn (forbidden)
- var touchingSpawn = Math.abs(self.gridX - spawn.x) + Math.abs(self.gridY - spawn.y) === 1;
+ var centerX = self.gridX + 1;
+ var centerY = self.gridY + 1;
+ var touchingSpawn = Math.abs(centerX - spawn.x) + Math.abs(centerY - spawn.y) === 1;
if (touchingSpawn) {
self.canPlace = false;
} else {
// Check if center cell is adjacent to current base (end of current path)
@@ -946,10 +948,13 @@
x: base.x - 1,
y: base.y
} // left
];
+ // Check if center of 3x3 block (gridX+1, gridY+1) is adjacent to base
+ var centerX = self.gridX + 1;
+ var centerY = self.gridY + 1;
for (var i = 0; i < baseNeighbors.length; i++) {
- if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) {
+ if (baseNeighbors[i].x === centerX && baseNeighbors[i].y === centerY) {
isAdjacentToBase = true;
break;
}
}
@@ -985,12 +990,14 @@
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
- self.gridX = Math.floor(gridPosX / CELL_SIZE);
- self.gridY = Math.floor(gridPosY / CELL_SIZE);
- self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE * ROAD_SIZE / 2;
- self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE * ROAD_SIZE / 2;
+ // Get center cell position, then adjust to get top-left of 3x3 block
+ self.gridX = Math.floor(gridPosX / CELL_SIZE) - 1;
+ self.gridY = Math.floor(gridPosY / CELL_SIZE) - 1;
+ // Position at center of 3x3 block: (gridX + 1.5) * CELL_SIZE
+ self.x = grid.x + (self.gridX + 1.5) * CELL_SIZE;
+ self.y = grid.y + (self.gridY + 1.5) * CELL_SIZE;
self.updatePlacementStatus();
};
return self;
});
@@ -1181,10 +1188,11 @@
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
- var centerX = self.gridX + 1;
- var centerY = self.gridY + 1;
+ // Center of 3x3 block
+ var centerX = self.gridX + 1.5;
+ var centerY = self.gridY + 1.5;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
@@ -1500,10 +1508,11 @@
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
- self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE * TOWER_SIZE / 2;
- self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE * TOWER_SIZE / 2;
+ // Position at center of 3x3 block: (gridX + 1.5) * CELL_SIZE
+ self.x = grid.x + (gridX + 1.5) * CELL_SIZE;
+ self.y = grid.y + (gridY + 1.5) * CELL_SIZE;
for (var i = 0; i < TOWER_SIZE; i++) {
for (var j = 0; j < TOWER_SIZE; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
@@ -1678,12 +1687,14 @@
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
- self.gridX = Math.floor(gridPosX / CELL_SIZE);
- self.gridY = Math.floor(gridPosY / CELL_SIZE);
- self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE * TOWER_SIZE / 2;
- self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE * TOWER_SIZE / 2;
+ // Get center cell position, then adjust to get top-left of 3x3 block
+ self.gridX = Math.floor(gridPosX / CELL_SIZE) - 1;
+ self.gridY = Math.floor(gridPosY / CELL_SIZE) - 1;
+ // Position at center of 3x3 block: (gridX + 1.5) * CELL_SIZE
+ self.x = grid.x + (self.gridX + 1.5) * CELL_SIZE;
+ self.y = grid.y + (self.gridY + 1.5) * CELL_SIZE;
self.checkPlacement();
};
return self;
});
@@ -2578,9 +2589,12 @@
isDragging = false;
if (dragType === 'road') {
// Handle road placement
if (roadPreview.canPlace && gold >= ROAD_COST) {
- var centerCell = grid.getCell(roadPreview.gridX, roadPreview.gridY);
+ // Get center cell of 3x3 block
+ var centerX = roadPreview.gridX + 1;
+ var centerY = roadPreview.gridY + 1;
+ var centerCell = grid.getCell(centerX, centerY);
if (centerCell) {
// 2-1) Deduct gold
setGold(gold - ROAD_COST);
// 2-2) Convert old base block to normal road
@@ -2597,9 +2611,9 @@
}
// 2-3) Make new center cell and surrounding cells the base
for (var i = -1; i <= 1; i++) {
for (var j = -1; j <= 1; j++) {
- var newCell = grid.getCell(centerCell.x + i, centerCell.y + j);
+ var newCell = grid.getCell(centerX + i, centerY + j);
if (newCell) {
newCell.type = 3; // make this the new base
newCell.hasTower = false;
}
@@ -2607,10 +2621,10 @@
}
grid.goals[0] = centerCell; // update goals array to center
// 2-4) Update pathPoints array for debug arrows
pathPoints.push({
- x: centerCell.x,
- y: centerCell.y
+ x: centerX,
+ y: centerY
});
// 2-5) Recalculate path and redraw
grid.pathFind();
grid.renderDebug();
17. century tower castle square shaped 1x1 medeval military base,grey coloured top-down look , simple design, medieval-semirealistic.. In-Game asset. 2d. High contrast. No shadows
17.th century cannon bullet semirealistic medieval cannon bullet. In-Game asset. 2d. High contrast. No shadows
top down look for basic cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
red coloured top down look for quick cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows.
armor grey coloured top down look for armoured basic cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
swarm enemy light green coloured top down look for quick cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows.