/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // BuildTile class (for tower placement) var BuildTile = Container.expand(function () { var self = Container.call(this); // Attach tile asset (box, light gray) var tileAsset = self.attachAsset('buildTile', { anchorX: 0.5, anchorY: 0.5 }); self.occupied = false; self.tower = null; // For highlighting self.highlight = function (on) { tileAsset.tint = on ? 0x2ecc40 : 0xbdc3c7; }; return self; }); // Bullet class var Bullet = Container.expand(function () { var self = Container.call(this); // Attach bullet asset (small yellow box) var bulletAsset = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.target = null; self.damage = 5; self.speed = 24; self.update = function () { if (!self.target || self.target.health <= 0 || self.target.reachedBase) { self.destroyed = true; return; } var dx = self.target.x - self.x; var dy = self.target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 32) { // Hit self.target.takeDamage(self.damage); self.destroyed = true; return; } var moveDist = Math.min(self.speed, dist); self.x += dx / dist * moveDist; self.y += dy / dist * moveDist; }; return self; }); // Enemy (Creep) class var Enemy = Container.expand(function () { var self = Container.call(this); // Attach enemy asset (ellipse, purple) var enemyAsset = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); // Enemy stats self.maxHealth = Math.round(ENEMY_BASE_HEALTH * currentWave * ENEMY_HEALTH_WAVE_MULTIPLIER); self.health = self.maxHealth; self.speed = 2 + (currentWave - 1) * 0.2; // Slightly faster each wave self.reward = 5 + (currentWave - 1) * 2; // Path progress self.pathIndex = 0; self.pathProgress = 0; // For hit flash self.isFlashing = false; // Move along path self.update = function () { if (self.pathIndex >= path.length - 1) { return; } var from = path[self.pathIndex]; var to = path[self.pathIndex + 1]; var dx = to.x - from.x; var dy = to.y - from.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist === 0) { self.pathIndex++; self.pathProgress = 0; return; } var moveDist = self.speed; self.pathProgress += moveDist; if (self.pathProgress >= dist) { self.pathIndex++; self.pathProgress = 0; if (self.pathIndex >= path.length - 1) { // Reached end self.x = to.x; self.y = to.y; self.reachedBase = true; return; } } var t = self.pathProgress / dist; self.x = from.x + dx * t; self.y = from.y + dy * t; }; // Take damage self.takeDamage = function (dmg) { self.health -= dmg; if (!self.isFlashing) { self.isFlashing = true; tween(enemyAsset, { tint: 0xffffff }, { duration: 80, onFinish: function onFinish() { tween(enemyAsset, { tint: 0x8e44ad }, { duration: 120, onFinish: function onFinish() { self.isFlashing = false; } }); } }); } }; return self; }); // Tower class var Tower = Container.expand(function () { var self = Container.call(this); // Attach tower asset (box, blue) var towerAsset = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // Double-tap boost: track last tap time self._lastTapTime = 0; // Level label (bottom-left) self.level = 1; // Default level is 1 self.levelLabel = new Text2(self.level + '', { size: 36, fill: "#fff" }); self.levelLabel.anchor.set(0, 1); // bottom-left // Position at bottom-left of tower asset self.levelLabel.x = -50; self.levelLabel.y = 50; self.addChild(self.levelLabel); // Delete button (top-right of tower) self.deleteBtn = new Text2('✖', { size: 40, fill: "#fff" }); self.deleteBtn.anchor.set(1, 0); // top-right self.deleteBtn.x = 50; self.deleteBtn.y = -50; self.addChild(self.deleteBtn); // Delete logic self.deleteBtn.down = function (x, y, obj) { // Remove from towers array for (var i = towers.length - 1; i >= 0; i--) { if (towers[i] === self) { towers.splice(i, 1); break; } } // Remove from parent self.destroy(); }; // Each tower instance manages its own stats and upgrades self.range = 320; self.damage = 5; self.fireRate = 60; // frames per shot self.cooldown = 0; // For upgrade flash self.isFlashing = false; // Show range circle (for placement) self.rangeCircle = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.15 }); self.rangeCircle.width = self.range * 2; self.rangeCircle.height = self.range * 2; self.rangeCircle.visible = false; // Double-tap to boost this tower only self.down = function (x, y, obj) { var now = Date.now(); if (self._lastTapTime && now - self._lastTapTime < 400) { // Double-tap detected: try to upgrade this tower // Find color index for this tower var colorIdx = 0; if (self.children && self.children.length > 0) { var tint = self.children[0].tint; for (var i = 0; i < TOWER_SELECT_COLORS.length; i++) { if (TOWER_SELECT_COLORS[i] === tint) { colorIdx = i; break; } } } // Calculate upgrade cost: base * current level var upgradeCost = towerCurrentPrices[colorIdx] * self.level; if (money >= upgradeCost) { money -= upgradeCost; // Increase all tower prices by tower price multiplier globally for (var i = 0; i < towerCurrentPrices.length; i++) { towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER); } // Actually upgrade tower self.level++; self.damage = Math.round(self.damage * TOWER_DAMAGE_UPGRADE_MULTIPLIER); self.fireRate = Math.max(1, Math.round(self.fireRate / TOWER_SPEED_UPGRADE_MULTIPLIER)); // Update level label self.levelLabel.setText(self.level + ''); if (typeof moneyTxt !== "undefined") { moneyTxt.setText('Money: ' + money); } } } self._lastTapTime = now; }; // Upgrade tower (for upgrade button or other triggers) self.upgrade = function () { if (self.level >= 3) { return false; } self.level++; if (self.levelLabel) { self.levelLabel.setText(self.level + ''); } // Find color index for this tower var colorIdx = 0; if (self.children && self.children.length > 0) { var tint = self.children[0].tint; for (var i = 0; i < TOWER_SELECT_COLORS.length; i++) { if (TOWER_SELECT_COLORS[i] === tint) { colorIdx = i; break; } } } // Upgrade logic: scale up stats based on color if (typeof TOWER_STATS !== "undefined" && TOWER_STATS[colorIdx]) { self.damage += Math.round(TOWER_STATS[colorIdx].damage * 0.7); self.range += Math.round(TOWER_STATS[colorIdx].range * 0.12); self.fireRate = Math.max(20, self.fireRate - Math.round(TOWER_STATS[colorIdx].fireRate * 0.18)); } else { self.damage += TOWER_DAMAGE_BLUE; self.range += Math.round(TOWER_RANGE_STANDARD * 0.125); self.fireRate = Math.max(30, self.fireRate - 10); } self.rangeCircle.width = self.range * 2; self.rangeCircle.height = self.range * 2; // Flash for upgrade if (!self.isFlashing) { self.isFlashing = true; tween(towerAsset, { tint: 0xffff00 }, { duration: 120, onFinish: function onFinish() { tween(towerAsset, { tint: 0x3498db }, { duration: 180, onFinish: function onFinish() { self.isFlashing = false; } }); } }); } return true; }; // Tower attack logic self.update = function () { if (self.cooldown > 0) { self.cooldown--; return; } // Find nearest enemy in range var nearest = null; var minDist = 99999; // Determine if this is a purple tower (Mor) var isPurple = false; if (self.children && self.children.length > 0 && self.children[0].tint === 0x8e44ad) { isPurple = true; } for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if ((isPurple || dist <= self.range) && dist < minDist) { nearest = e; minDist = dist; } } if (nearest) { // Shoot var b = new Bullet(); b.x = self.x; b.y = self.y; b.target = nearest; b.damage = self.damage; b.speed = 24; b.lastX = b.x; b.lastY = b.y; bullets.push(b); game.addChild(b); self.cooldown = self.fireRate; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // --- Game Constants and Multipliers --- // Tower stats constants var TOWER_DAMAGE_BLUE = 5; // Mavi (blue) tower damage var TOWER_DAMAGE_RED = 15; // Kırmızı (red) tower damage var TOWER_DAMAGE_GREEN = 5; // Yeşil (green) tower damage var TOWER_DAMAGE_YELLOW = 50; // Sarı (yellow) tower damage var TOWER_DAMAGE_PURPLE = 15; // Mor (purple) tower damage var TOWER_FIRE_RATE_BLUE = 60; // Mavi (blue) tower fire rate (frames per shot) var TOWER_FIRE_RATE_RED = 60; // Kırmızı (red) tower fire rate var TOWER_FIRE_RATE_GREEN = 20; // Yeşil (green) tower fire rate (3x faster than blue) var TOWER_FIRE_RATE_YELLOW = 120; // Sarı (yellow) tower fire rate (2 seconds per shot) var TOWER_FIRE_RATE_PURPLE = 120; // Mor (purple) tower fire rate (2 seconds per shot) var TOWER_RANGE_STANDARD = 320; // Standard tower range var TOWER_RANGE_GLOBAL = 2048; // Global range for purple tower // Game progression multipliers var ENEMY_BASE_HEALTH = 10; // Base health for enemies var ENEMY_HEALTH_WAVE_MULTIPLIER = 1.25; // Health multiplier per wave var TOWER_PRICE_INCREASE_MULTIPLIER = 1.25; // Tower price increase multiplier // Tower upgrade multipliers var TOWER_DAMAGE_UPGRADE_MULTIPLIER = 1.1; // Damage increase on upgrade var TOWER_SPEED_UPGRADE_MULTIPLIER = 1.1; // Fire rate improvement on upgrade // --- Grid system --- // Set grid to 10x12 with margins, leave 1 row margin at bottom, and add 5 tower selection squares below grid var GRID_COLS = 10; var GRID_ROWS = 12; var GRID_SIZE = 160; // Calculate margins to center grid in 2048x2732, leaving 1 row margin at bottom for spacing var GRID_TOTAL_WIDTH = GRID_COLS * GRID_SIZE; var GRID_TOTAL_HEIGHT = GRID_ROWS * GRID_SIZE; var GRID_MARGIN_X = Math.floor((2048 - GRID_TOTAL_WIDTH) / 2); var GRID_MARGIN_Y = Math.floor((2732 - (GRID_TOTAL_HEIGHT + GRID_SIZE)) / 2); // +GRID_SIZE for 1 row margin at bottom // Helper to snap to grid (with margin) function snapToGrid(x, y) { return { x: GRID_MARGIN_X + Math.round((x - GRID_MARGIN_X) / GRID_SIZE) * GRID_SIZE, y: GRID_MARGIN_Y + Math.round((y - GRID_MARGIN_Y) / GRID_SIZE) * GRID_SIZE }; } function generateSimplePath() { var pathArr = []; var visited = {}; // Ziyaret edilen hücreler ve geçiş yönleri: { h: boolean, v: boolean } var yukariDonusYapildi = false; // YENİ: Yukarı dönüş yapılıp yapılmadığını takip eder function cellKey(c, r) { return c + "," + r; } var startCols = []; for (var c = 1; c < GRID_COLS - 1; c++) { startCols.push(c); } if (startCols.length === 0) { return [snapToGrid(GRID_MARGIN_X, GRID_MARGIN_Y)]; } var startCol = startCols[Math.floor(Math.random() * startCols.length)]; var curCol = startCol; var curRow = 0; pathArr.push(snapToGrid(GRID_MARGIN_X + curCol * GRID_SIZE, GRID_MARGIN_Y + curRow * GRID_SIZE)); var startKey = cellKey(curCol, curRow); visited[startKey] = { h: false, v: true }; // Başlangıçta aşağı varsayımı curRow = 1; if (GRID_ROWS < 2) { return pathArr; } pathArr.push(snapToGrid(GRID_MARGIN_X + curCol * GRID_SIZE, GRID_MARGIN_Y + curRow * GRID_SIZE)); var firstStepKey = cellKey(curCol, curRow); visited[firstStepKey] = visited[firstStepKey] || { h: false, v: false }; visited[firstStepKey].v = true; var endCols = []; for (var c = 1; c < GRID_COLS - 1; c++) { endCols.push(c); } if (endCols.length === 0) { return pathArr; } var endCol = endCols[Math.floor(Math.random() * endCols.length)]; var endRow = GRID_ROWS - 1; var preEndRow = GRID_ROWS - 2; if (preEndRow < curRow) { if (GRID_ROWS > 0) { pathArr.push(snapToGrid(GRID_MARGIN_X + endCol * GRID_SIZE, GRID_MARGIN_Y + endRow * GRID_SIZE)); // visited[cellKey(endCol, endRow)] ... } return pathArr; } var lastDir = "down"; var straightCount = 1; var maxRetries = GRID_COLS * GRID_ROWS * 15; // Biraz daha fazla deneme hakkı var retries = 0; while (!(curRow === preEndRow && curCol === endCol)) { retries++; if (retries > maxRetries) { // console.warn("Path Gen: Max retries! Yukari Donus: " + yukariDonusYapildi); break; } var potentialMoves = []; // Potansiyel hamleleri oluştur (önceliklendirme olmadan tüm yönleri düşün) if (curRow < preEndRow && lastDir !== "up") { potentialMoves.push({ name: "down", dx: 0, dy: 1 }); } if (curRow > 1 && lastDir !== "down") { potentialMoves.push({ name: "up", dx: 0, dy: -1 }); } if (curCol < GRID_COLS - 2 && lastDir !== "left") { potentialMoves.push({ name: "right", dx: 1, dy: 0 }); } if (curCol > 1 && lastDir !== "right") { potentialMoves.push({ name: "left", dx: -1, dy: 0 }); } // Eğer hiçbir potansiyel hamle yoksa (örn. 180 derece dönüş engeli nedeniyle), temel hamleleri tekrar ekle if (potentialMoves.length === 0) { if (curRow < preEndRow) { potentialMoves.push({ name: "down", dx: 0, dy: 1 }); } if (curRow > 1) { potentialMoves.push({ name: "up", dx: 0, dy: -1 }); } if (curCol < GRID_COLS - 2) { potentialMoves.push({ name: "right", dx: 1, dy: 0 }); } if (curCol > 1) { potentialMoves.push({ name: "left", dx: -1, dy: 0 }); } } var validMovesRaw = []; for (var i = 0; i < potentialMoves.length; i++) { var move = potentialMoves[i]; var testCol = curCol + move.dx; var testRow = curRow + move.dy; var isHorizMove = move.dy === 0; if (testCol < 1 || testCol > GRID_COLS - 2 || testRow < 1 || testRow > preEndRow) { continue; } var vDataTarget = visited[cellKey(testCol, testRow)]; if (vDataTarget) { if (isHorizMove && vDataTarget.h) { continue; } if (!isHorizMove && vDataTarget.v) { continue; } } if (move.name === "up" && lastDir !== "up") { // Sadece gerçek bir "yukarı dönüş" ise kontrol et var upTurnSpacingOk = true; for (var side = -1; side <= 1; side += 2) { for (var dist = 1; dist <= 3; dist++) { var checkC = testCol + side * dist; if (checkC < 1 || checkC > GRID_COLS - 2) { upTurnSpacingOk = false; break; } var nKey = cellKey(checkC, testRow); var vDataNeighbor = visited[nKey]; if (vDataNeighbor && (vDataNeighbor.h || vDataNeighbor.v)) { upTurnSpacingOk = false; break; } } if (!upTurnSpacingOk) { break; } } if (!upTurnSpacingOk) { continue; } } var parallelSpacingOk = true; if (isHorizMove) { var nAboveKey = cellKey(testCol, testRow - 1); var vAbove = visited[nAboveKey]; var nBelowKey = cellKey(testCol, testRow + 1); var vBelow = visited[nBelowKey]; if (vAbove && vAbove.h && testRow - 1 !== curRow || vBelow && vBelow.h && testRow + 1 !== curRow) { parallelSpacingOk = false; } } else { var nLeftKey = cellKey(testCol - 1, testRow); var vLeft = visited[nLeftKey]; var nRightKey = cellKey(testCol + 1, testRow); var vRight = visited[nRightKey]; if (vLeft && vLeft.v && testCol - 1 !== curCol || vRight && vRight.v && testCol + 1 !== curCol) { parallelSpacingOk = false; } } if (!parallelSpacingOk) { continue; } validMovesRaw.push(move); } if (validMovesRaw.length === 0) { /*console.warn("No valid raw moves for " + curCol + "," + curRow);*/ break; } var currentActionableMoves = validMovesRaw; if (straightCount >= 4) { var nonStraightMoves = currentActionableMoves.filter(function (m) { return m.name !== lastDir; }); if (nonStraightMoves.length > 0) { currentActionableMoves = nonStraightMoves; } } if (currentActionableMoves.length === 0 && validMovesRaw.length > 0) { currentActionableMoves = validMovesRaw; } if (currentActionableMoves.length === 0) { /*console.warn("No actionable after straight count for " + curCol + "," + curRow);*/ break; } var selectedMove = null; var potentialUpwardTurn = null; if (!yukariDonusYapildi) { currentActionableMoves.forEach(function (m) { if (m.name === "up" && lastDir !== "up") { potentialUpwardTurn = m; // Kurallara uyan bir yukarı dönüş bulundu } }); } // Hamle Seçim Mantığı if (potentialUpwardTurn) { // Eğer yukarı dönüş yapılmadıysa ve mümkünse, onu önceliklendir selectedMove = potentialUpwardTurn; } else { // Normal hamle seçim mantığı (düz gitme, hedefe yönelme vb.) var moveInLastDir = null; currentActionableMoves.forEach(function (m) { if (m.name === lastDir) { moveInLastDir = m; } }); var preferredMovesTowardsTarget = []; currentActionableMoves.forEach(function (m) { var candCol = curCol + m.dx; var candRow = curRow + m.dy; if (candCol === endCol && candRow === preEndRow) { preferredMovesTowardsTarget.unshift(m); } else { var distOld = Math.abs(curCol - endCol) + Math.abs(curRow - preEndRow); var distNew = Math.abs(candCol - endCol) + Math.abs(candRow - preEndRow); if (distNew < distOld) { preferredMovesTowardsTarget.push(m); } } }); var straightAndPreferredTarget = null; if (moveInLastDir && straightCount < 4) { preferredMovesTowardsTarget.forEach(function (pm) { if (pm.name === moveInLastDir.name) { straightAndPreferredTarget = moveInLastDir; } }); } if (straightAndPreferredTarget && Math.random() < 0.75) { // %75 düz ve hedefe yönelik selectedMove = straightAndPreferredTarget; } else if (preferredMovesTowardsTarget.length > 0 && Math.random() < 0.80) { // %80 hedefe yönelik selectedMove = preferredMovesTowardsTarget[Math.floor(Math.random() * preferredMovesTowardsTarget.length)]; } else if (moveInLastDir && straightCount < 4 && Math.random() < 0.65) { // %65 düz git selectedMove = moveInLastDir; } else if (currentActionableMoves.length > 0) { // Son çare selectedMove = currentActionableMoves[Math.floor(Math.random() * currentActionableMoves.length)]; } else { break; } } if (!selectedMove && currentActionableMoves.length > 0) { // Eğer yukarıdaki mantıkla seçilemediyse (örn. potentialUpwardTurn vardı ama seçilmedi) selectedMove = currentActionableMoves[Math.floor(Math.random() * currentActionableMoves.length)]; } if (!selectedMove) { /*console.warn("No move selected for " + curCol + "," + curRow);*/ break; } // Seçilen hamleyi uygula ve durumu güncelle if (selectedMove.name === "up" && lastDir !== "up") { yukariDonusYapildi = true; } var movedHorizontally = selectedMove.dy === 0; if (selectedMove.name === lastDir) { straightCount++; } else { straightCount = 1; } lastDir = selectedMove.name; curCol += selectedMove.dx; curRow += selectedMove.dy; var newKey = cellKey(curCol, curRow); visited[newKey] = visited[newKey] || { h: false, v: false }; if (movedHorizontally) { visited[newKey].h = true; } else { visited[newKey].v = true; } pathArr.push(snapToGrid(GRID_MARGIN_X + curCol * GRID_SIZE, GRID_MARGIN_Y + curRow * GRID_SIZE)); } // while sonu // Eğer döngü bittiğinde hala yukarı dönüş yapılmadıysa ve yol çok kısaysa, bu bir sorun olabilir. // Ancak algoritma, mümkün olan en kısa sürede yukarı dönüş yapmaya çalışacaktır. // if (!yukariDonusYapildi && pathArr.length > (GRID_ROWS / 2)) console.log("Uyarı: Yol tamamlandı ancak yukarı dönüş yapılamadı."); if (GRID_ROWS > 0) { pathArr.push(snapToGrid(GRID_MARGIN_X + endCol * GRID_SIZE, GRID_MARGIN_Y + endRow * GRID_SIZE)); // visited[cellKey(endCol, endRow)] ... } return pathArr; } var path = generateSimplePath(); // --- Start Game Button Logic --- var gameStarted = false; var startBtn = new Text2('▶ Start Game', { size: 120, fill: 0x27AE60 }); startBtn.anchor.set(0.5, 0.5); LK.gui.center.addChild(startBtn); function setStartBtnVisible(visible) { startBtn.visible = visible; } setStartBtnVisible(true); startBtn.down = function (x, y, obj) { if (!gameStarted) { gameStarted = true; setStartBtnVisible(false); setNextWaveBtnVisible(true); startWave(); // Start the first wave immediately } }; // Hide all gameplay UI until game starts setNextWaveBtnVisible(false); if (typeof kpTxt !== "undefined") { kpTxt.visible = false; } if (typeof healthTxt !== "undefined") { healthTxt.visible = false; } if (typeof waveTxt !== "undefined") { waveTxt.visible = false; } if (typeof nextWaveBtn !== "undefined") { nextWaveBtn.visible = false; } // autoNextWaveBtn is always visible; do not hide // Show gameplay UI when game starts function showGameplayUI() { kpTxt.visible = true; healthTxt.visible = true; waveTxt.visible = true; nextWaveBtn.visible = false; nextWaveBtn.x = 0; nextWaveBtn.y = kpTxt.y + kpTxt.height + 20; // autoNextWaveBtn is always visible and positioned above waveTxt; do not reposition or show/hide here kpTxt.setText('Points: ' + knowledgePoints); healthTxt.setText('Base: ' + baseHealth); waveTxt.setText('Wave: ' + currentWave + '/' + maxWaves); } var _origStartWave = startWave; startWave = function startWave() { if (!gameStarted) { return; } showGameplayUI(); _origStartWave(); }; // Buildable tile logic removed for now var buildTilePositions = []; // Global game state var enemies = []; var towers = []; var bullets = []; // buildTiles removed for now var baseHealth = 10; var scoreo = 0; // Killed enemy count, used as score only var money = 50; // Used for purchases, start with 50 var knowledgePoints = 30; // Deprecated, kept for UI compatibility var currentWave = 1; var maxWaves = 999999; // Effectively infinite waves var waveInProgress = false; var waveTimer = null; var spawnIndex = 0; var spawnDelay = 40; // frames between spawns var enemiesToSpawn = 0; var selectedTile = null; // Tower price system var towerBasePrices = [30, 50, 40, 60, 80]; // Starting price for each tower type (by power) var towerCurrentPrices = towerBasePrices.slice(); // Current price for each type // Asset initialization // Draw grid background (light gray squares) with margins and 10x12 grid area for (var row = 0; row < GRID_ROWS; row++) { for (var col = 0; col < GRID_COLS; col++) { var gridNode = LK.getAsset('buildTile', { anchorX: 0.5, anchorY: 0.5, width: GRID_SIZE - 4, height: GRID_SIZE - 4, color: 0xf4f4f4, shape: 'box', alpha: 0.18 }); gridNode.x = GRID_MARGIN_X + col * GRID_SIZE; gridNode.y = GRID_MARGIN_Y + row * GRID_SIZE; game.addChild(gridNode); } } // Draw 5 colored tower selection squares below the grid, centered horizontally var TOWER_SELECT_COUNT = 5; var TOWER_SELECT_SIZE = 140; var TOWER_SELECT_SPACING = 60; var TOWER_SELECT_COLORS = [0x3498db, 0xe74c3c, 0x2ecc40, 0xf1c40f, 0x8e44ad]; var TOWER_SELECT_LABELS = ['Mavi', 'Kırmızı', 'Yeşil', 'Sarı', 'Mor']; // Tower stats for each color: {damage, range, fireRate} var TOWER_STATS = [{ damage: TOWER_DAMAGE_BLUE, range: TOWER_RANGE_STANDARD, fireRate: TOWER_FIRE_RATE_BLUE }, // Mavi { damage: TOWER_DAMAGE_RED, range: TOWER_RANGE_STANDARD, fireRate: TOWER_FIRE_RATE_RED }, // Kırmızı { damage: TOWER_DAMAGE_GREEN, range: TOWER_RANGE_STANDARD, fireRate: TOWER_FIRE_RATE_GREEN }, // Yeşil { damage: TOWER_DAMAGE_YELLOW, range: TOWER_RANGE_STANDARD, fireRate: TOWER_FIRE_RATE_YELLOW }, // Sarı { damage: TOWER_DAMAGE_PURPLE, range: TOWER_RANGE_GLOBAL, fireRate: TOWER_FIRE_RATE_PURPLE } // Mor (global range) ]; var towerSelectSquares = []; var totalWidth = TOWER_SELECT_COUNT * TOWER_SELECT_SIZE + (TOWER_SELECT_COUNT - 1) * TOWER_SELECT_SPACING; var startX = Math.floor((2048 - totalWidth) / 2) + TOWER_SELECT_SIZE / 2; var selectY = GRID_MARGIN_Y + GRID_ROWS * GRID_SIZE + GRID_SIZE / 2; for (var i = 0; i < TOWER_SELECT_COUNT; i++) { var square = LK.getAsset('buildTile', { anchorX: 0.5, anchorY: 0.5, width: TOWER_SELECT_SIZE, height: TOWER_SELECT_SIZE, color: TOWER_SELECT_COLORS[i], shape: 'box', alpha: 0.95 }); square.x = startX + i * (TOWER_SELECT_SIZE + TOWER_SELECT_SPACING); square.y = selectY; game.addChild(square); // Add label for each tower color var label = new Text2(TOWER_SELECT_LABELS[i], { size: 48, fill: "#fff" }); label.anchor.set(0.5, 0); label.x = square.x; label.y = square.y + TOWER_SELECT_SIZE / 2 + 8; game.addChild(label); towerSelectSquares.push(square); } // Draw path (for visual reference) for (var i = 0; i < path.length - 1; i++) { var from = path[i]; var to = path[i + 1]; var dx = to.x - from.x; var dy = to.y - from.y; var dist = Math.sqrt(dx * dx + dy * dy); var steps = Math.floor(dist / 40); for (var s = 0; s <= steps; s++) { var t = s / steps; var px = from.x + dx * t; var py = from.y + dy * t; var node = LK.getAsset('pathDot', { anchorX: 0.5, anchorY: 0.5, width: 32, height: 32, color: 0x7f8c8d, shape: 'ellipse' }); node.x = px; node.y = py; game.addChild(node); } } // Buildable tile objects removed for now // --- Tower Selection Bar --- var towerTypes = []; var selectedTowerType = null; var towerButtons = []; var towerBtnY = 2400; var towerBtnSpacing = 260; var towerBtnStartX = 400; var draggingTowerType = null; var draggingTowerGhost = null; // No tower selection buttons since normal tower is removed // --- Drag-and-drop tower placement from colored squares --- for (var i = 0; i < towerSelectSquares.length; i++) { (function (colorIdx) { var square = towerSelectSquares[colorIdx]; square.down = function (x, y, obj) { // Clean up any previous ghost if (draggingTowerGhost) { if (draggingTowerGhost.rangeCircle) { draggingTowerGhost.rangeCircle.destroy(); } draggingTowerGhost.destroy(); draggingTowerGhost = null; draggingTowerType = null; } draggingTowerType = colorIdx; draggingTowerGhost = new Tower(); // Snap to grid for initial drag position var gridX = Math.round((x - GRID_MARGIN_X) / GRID_SIZE); var gridY = Math.round((y - GRID_MARGIN_Y) / GRID_SIZE); var snapX = GRID_MARGIN_X + gridX * GRID_SIZE; var snapY = GRID_MARGIN_Y + gridY * GRID_SIZE; draggingTowerGhost.x = snapX; draggingTowerGhost.y = snapY; // Set color of ghost tower if (draggingTowerGhost.children && draggingTowerGhost.children.length > 0) { draggingTowerGhost.children[0].tint = TOWER_SELECT_COLORS[colorIdx]; } // Set stats based on color for ghost if (typeof TOWER_STATS !== "undefined" && TOWER_STATS[colorIdx]) { draggingTowerGhost.damage = TOWER_STATS[colorIdx].damage; draggingTowerGhost.range = TOWER_STATS[colorIdx].range; draggingTowerGhost.fireRate = TOWER_STATS[colorIdx].fireRate; if (draggingTowerGhost.rangeCircle) { draggingTowerGhost.rangeCircle.width = draggingTowerGhost.range * 2; draggingTowerGhost.rangeCircle.height = draggingTowerGhost.range * 2; } } // Show range circle while dragging, and center it on the ghost if (draggingTowerGhost.rangeCircle) { draggingTowerGhost.rangeCircle.visible = true; draggingTowerGhost.rangeCircle.x = 0; draggingTowerGhost.rangeCircle.y = 0; } draggingTowerGhost.alpha = 0.7; game.addChild(draggingTowerGhost); }; })(i); } // Helper: check if a grid cell is on the path function isCellOnPath(gridX, gridY) { for (var i = 0; i < path.length; i++) { var px = path[i].x; var py = path[i].y; var cellX = Math.round((px - GRID_MARGIN_X) / GRID_SIZE); var cellY = Math.round((py - GRID_MARGIN_Y) / GRID_SIZE); if (cellX === gridX && cellY === gridY) { return true; } } return false; } // Helper: check if a tower already exists at grid cell function isTowerAtCell(gridX, gridY) { for (var i = 0; i < towers.length; i++) { var t = towers[i]; var tx = Math.round((t.x - GRID_MARGIN_X) / GRID_SIZE); var ty = Math.round((t.y - GRID_MARGIN_Y) / GRID_SIZE); if (tx === gridX && ty === gridY) { return true; } } return false; } // Dragging ghost tower with finger/mouse game.move = function (x, y, obj) { if (draggingTowerGhost) { // Snap ghost to grid for feedback var gridX = Math.round((x - GRID_MARGIN_X) / GRID_SIZE); var gridY = Math.round((y - GRID_MARGIN_Y) / GRID_SIZE); var snapX = GRID_MARGIN_X + gridX * GRID_SIZE; var snapY = GRID_MARGIN_Y + gridY * GRID_SIZE; draggingTowerGhost.x = snapX; draggingTowerGhost.y = snapY; // Always center rangeCircle on the ghost (0,0 in local coordinates) if (draggingTowerGhost.rangeCircle) { draggingTowerGhost.rangeCircle.x = 0; draggingTowerGhost.rangeCircle.y = 0; } // Visual feedback: tint red if invalid, normal if valid var valid = true; // Only allow inside grid if (gridX < 0 || gridX >= GRID_COLS || gridY < 0 || gridY >= GRID_ROWS || isCellOnPath(gridX, gridY) || isTowerAtCell(gridX, gridY)) { valid = false; } if (draggingTowerGhost.children && draggingTowerGhost.children.length > 0) { draggingTowerGhost.children[0].tint = valid ? TOWER_SELECT_COLORS[draggingTowerType] : 0x888888; } draggingTowerGhost.alpha = valid ? 0.7 : 0.35; } }; // Place tower on grid on up game.up = function (x, y, obj) { if (draggingTowerGhost && draggingTowerType !== null) { var gridX = Math.round((draggingTowerGhost.x - GRID_MARGIN_X) / GRID_SIZE); var gridY = Math.round((draggingTowerGhost.y - GRID_MARGIN_Y) / GRID_SIZE); var valid = true; if (gridX < 0 || gridX >= GRID_COLS || gridY < 0 || gridY >= GRID_ROWS || isCellOnPath(gridX, gridY) || isTowerAtCell(gridX, gridY)) { valid = false; } if (valid) { // Place tower if enough money (use dynamic price) var buildCost = towerCurrentPrices[draggingTowerType]; if (money >= buildCost) { money -= buildCost; // Increase all tower prices by tower price multiplier for (var i = 0; i < towerCurrentPrices.length; i++) { towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER); } var tower = new Tower(); tower.x = GRID_MARGIN_X + gridX * GRID_SIZE; tower.y = GRID_MARGIN_Y + gridY * GRID_SIZE; // Set color if (tower.children && tower.children.length > 0) { tower.children[0].tint = TOWER_SELECT_COLORS[draggingTowerType]; } // Set stats based on color if (typeof TOWER_STATS !== "undefined" && TOWER_STATS[draggingTowerType]) { tower.damage = TOWER_STATS[draggingTowerType].damage; tower.range = TOWER_STATS[draggingTowerType].range; tower.fireRate = TOWER_STATS[draggingTowerType].fireRate; if (tower.rangeCircle) { tower.rangeCircle.width = tower.range * 2; tower.rangeCircle.height = tower.range * 2; } } towers.push(tower); game.addChild(tower); moneyTxt.setText('Money: ' + money); } } // Clean up ghost if (draggingTowerGhost.rangeCircle) { draggingTowerGhost.rangeCircle.destroy(); } draggingTowerGhost.destroy(); draggingTowerGhost = null; draggingTowerType = null; return; } // If not placing, just clear ghost if (draggingTowerGhost) { if (draggingTowerGhost.rangeCircle) { draggingTowerGhost.rangeCircle.destroy(); } draggingTowerGhost.destroy(); draggingTowerGhost = null; draggingTowerType = null; } }; // Base indicator (ellipse, red) var baseNode = LK.getAsset('base', { anchorX: 0.5, anchorY: 0.5, width: 160, height: 160, color: 0xe74c3c, shape: 'ellipse' }); baseNode.x = path[path.length - 1].x; baseNode.y = path[path.length - 1].y; game.addChild(baseNode); // GUI: Points (KP) - Center top, smaller size, renamed to Points var kpTxt = new Text2('Score: ' + scoreo, { size: 48, fill: 0x2ecc40 // bright green, not transparent }); kpTxt.anchor.set(0.5, 0); // center aligned, top kpTxt.x = 0; kpTxt.y = 10; LK.gui.top.addChild(kpTxt); // GUI: Money - Bottom right var moneyTxt = new Text2('Money: ' + money, { size: 48, fill: 0xf1c40f }); moneyTxt.anchor.set(1, 1); // right aligned, bottom moneyTxt.x = -40; // 40px from right edge moneyTxt.y = -40; // 40px from bottom edge LK.gui.bottomRight.addChild(moneyTxt); // GUI: Base Health - Top right, smaller size var healthTxt = new Text2('Base: ' + baseHealth, { size: 48, fill: 0xE74C3C }); healthTxt.anchor.set(1, 0); // right aligned, top healthTxt.x = -40; // right margin healthTxt.y = 10; LK.gui.topRight.addChild(healthTxt); // GUI: Wave - Bottom left, smaller size var waveTxt = new Text2('Wave: ' + currentWave + '/' + maxWaves, { size: 48, fill: 0x2980B9 }); waveTxt.anchor.set(0, 1); // left aligned, bottom waveTxt.x = 40; waveTxt.y = -40; // Auto-next wave toggle (now above waveTxt, same size) var autoNextWave = false; var autoNextWaveBtn = new Text2('Auto: OFF', { size: 48, fill: 0x2980B9 }); autoNextWaveBtn.anchor.set(0, 1); autoNextWaveBtn.x = 40; autoNextWaveBtn.y = waveTxt.y - waveTxt.height - 8; // 8px gap above waveTxt LK.gui.bottomLeft.addChild(autoNextWaveBtn); LK.gui.bottomLeft.addChild(waveTxt); autoNextWaveBtn.visible = true; autoNextWaveBtn.down = function (x, y, obj) { autoNextWave = !autoNextWave; autoNextWaveBtn.setText('Auto: ' + (autoNextWave ? 'ON' : 'OFF')); }; // Show/hide next wave button function setNextWaveBtnVisible(visible) { // Defensive: Only set visible if nextWaveBtn is defined and has a visible property if (typeof nextWaveBtn !== "undefined" && nextWaveBtn && typeof nextWaveBtn.visible !== "undefined") { nextWaveBtn.visible = visible; } } setNextWaveBtnVisible(false); // Start next wave function startWave() { if (waveInProgress || currentWave > maxWaves) { return; } waveInProgress = true; setNextWaveBtnVisible(false); enemiesToSpawn = 6 + currentWave * 2; spawnIndex = 0; } // Next wave button (centered below Points text, always present but hidden when not needed) var nextWaveBtn = new Text2('Next Wave', { size: 64, fill: 0x2980B9 }); nextWaveBtn.anchor.set(0.5, 0); nextWaveBtn.x = 0; nextWaveBtn.y = kpTxt.y + kpTxt.height + 20; LK.gui.top.addChild(nextWaveBtn); nextWaveBtn.visible = false; // Next wave button event nextWaveBtn.down = function (x, y, obj) { if (!waveInProgress && currentWave <= maxWaves) { startWave(); } }; // Build/upgrade tower on tile function tryBuildOrUpgrade(tile) { // All upgrades and purchases use only money if (tile.occupied) { // Try upgrade var colorIdx = 0; if (tile.tower.children && tile.tower.children.length > 0) { var tint = tile.tower.children[0].tint; for (var i = 0; i < TOWER_SELECT_COLORS.length; i++) { if (TOWER_SELECT_COLORS[i] === tint) { colorIdx = i; break; } } } var upgradeCost = towerCurrentPrices[colorIdx] * tile.tower.level; if (tile.tower.level < 3) { if (money >= upgradeCost) { money -= upgradeCost; // Increase all tower prices by tower price multiplier globally for (var i = 0; i < towerCurrentPrices.length; i++) { towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER); } tile.tower.upgrade(); moneyTxt.setText('Money: ' + money); } } } else { // Build new tower var buildCost = towerCurrentPrices[0]; if (money >= buildCost) { money -= buildCost; // Increase all tower prices by tower price multiplier globally for (var i = 0; i < towerCurrentPrices.length; i++) { towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER); } var tower = new Tower(); tower.x = tile.x; tower.y = tile.y; towers.push(tower); game.addChild(tower); tile.occupied = true; tile.tower = tower; moneyTxt.setText('Money: ' + money); } } // Score is only for kill count, handled in enemy kill logic } // Highlight build tiles on touch game.down = function (x, y, obj) { // No tap-to-place logic; only drag-and-drop is supported for tower placement }; // Remove highlight/range on up // (handled by drag-and-drop up logic above) // Main game update game.update = function () { // Spawn enemies for wave if (waveInProgress && enemiesToSpawn > 0 && LK.ticks % spawnDelay === 0) { var enemy = new Enemy(); enemy.x = path[0].x; enemy.y = path[0].y; enemy.pathIndex = 0; enemy.pathProgress = 0; enemies.push(enemy); game.addChild(enemy); enemiesToSpawn--; } // Update enemies for (var i = enemies.length - 1; i >= 0; i--) { var e = enemies[i]; e.update(); if (e.health <= 0) { // Enemy defeated scoreo += 1; // Give money: 10 * currentWave var reward = currentWave * 10; money += reward; kpTxt.setText('Score: ' + scoreo); moneyTxt.setText('Money: ' + money); e.destroy(); enemies.splice(i, 1); } else if (e.reachedBase) { // Enemy reached base baseHealth--; healthTxt.setText('Base: ' + baseHealth); e.destroy(); enemies.splice(i, 1); healthTxt.setText('Base: ' + baseHealth); LK.effects.flashScreen(0xe74c3c, 400); if (baseHealth <= 0) { LK.showGameOver(); return; } } } // Update towers for (var i = 0; i < towers.length; i++) { towers[i].update(); } // Update bullets for (var i = bullets.length - 1; i >= 0; i--) { var b = bullets[i]; b.update(); if (b.destroyed) { b.destroy(); bullets.splice(i, 1); } } // End wave if all enemies defeated and none left to spawn if (waveInProgress && enemies.length === 0 && enemiesToSpawn === 0) { waveInProgress = false; currentWave++; // No win condition for infinite waves waveTxt.setText('Wave: ' + currentWave + '/' + maxWaves); // nextWaveBtn is repositioned, but autoNextWaveBtn is always visible and fixed above waveTxt nextWaveBtn.x = 0; nextWaveBtn.y = kpTxt.y + kpTxt.height + 20; // autoNextWaveBtn position is fixed above waveTxt; do not reposition or show/hide here if (autoNextWave) { setNextWaveBtnVisible(false); // Start next wave automatically after a short delay for feedback LK.setTimeout(function () { if (!waveInProgress && currentWave <= maxWaves) { startWave(); } }, 700); } else { setNextWaveBtnVisible(true); } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// BuildTile class (for tower placement)
var BuildTile = Container.expand(function () {
var self = Container.call(this);
// Attach tile asset (box, light gray)
var tileAsset = self.attachAsset('buildTile', {
anchorX: 0.5,
anchorY: 0.5
});
self.occupied = false;
self.tower = null;
// For highlighting
self.highlight = function (on) {
tileAsset.tint = on ? 0x2ecc40 : 0xbdc3c7;
};
return self;
});
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
// Attach bullet asset (small yellow box)
var bulletAsset = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.target = null;
self.damage = 5;
self.speed = 24;
self.update = function () {
if (!self.target || self.target.health <= 0 || self.target.reachedBase) {
self.destroyed = true;
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 32) {
// Hit
self.target.takeDamage(self.damage);
self.destroyed = true;
return;
}
var moveDist = Math.min(self.speed, dist);
self.x += dx / dist * moveDist;
self.y += dy / dist * moveDist;
};
return self;
});
// Enemy (Creep) class
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Attach enemy asset (ellipse, purple)
var enemyAsset = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Enemy stats
self.maxHealth = Math.round(ENEMY_BASE_HEALTH * currentWave * ENEMY_HEALTH_WAVE_MULTIPLIER);
self.health = self.maxHealth;
self.speed = 2 + (currentWave - 1) * 0.2; // Slightly faster each wave
self.reward = 5 + (currentWave - 1) * 2;
// Path progress
self.pathIndex = 0;
self.pathProgress = 0;
// For hit flash
self.isFlashing = false;
// Move along path
self.update = function () {
if (self.pathIndex >= path.length - 1) {
return;
}
var from = path[self.pathIndex];
var to = path[self.pathIndex + 1];
var dx = to.x - from.x;
var dy = to.y - from.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
self.pathIndex++;
self.pathProgress = 0;
return;
}
var moveDist = self.speed;
self.pathProgress += moveDist;
if (self.pathProgress >= dist) {
self.pathIndex++;
self.pathProgress = 0;
if (self.pathIndex >= path.length - 1) {
// Reached end
self.x = to.x;
self.y = to.y;
self.reachedBase = true;
return;
}
}
var t = self.pathProgress / dist;
self.x = from.x + dx * t;
self.y = from.y + dy * t;
};
// Take damage
self.takeDamage = function (dmg) {
self.health -= dmg;
if (!self.isFlashing) {
self.isFlashing = true;
tween(enemyAsset, {
tint: 0xffffff
}, {
duration: 80,
onFinish: function onFinish() {
tween(enemyAsset, {
tint: 0x8e44ad
}, {
duration: 120,
onFinish: function onFinish() {
self.isFlashing = false;
}
});
}
});
}
};
return self;
});
// Tower class
var Tower = Container.expand(function () {
var self = Container.call(this);
// Attach tower asset (box, blue)
var towerAsset = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
// Double-tap boost: track last tap time
self._lastTapTime = 0;
// Level label (bottom-left)
self.level = 1; // Default level is 1
self.levelLabel = new Text2(self.level + '', {
size: 36,
fill: "#fff"
});
self.levelLabel.anchor.set(0, 1); // bottom-left
// Position at bottom-left of tower asset
self.levelLabel.x = -50;
self.levelLabel.y = 50;
self.addChild(self.levelLabel);
// Delete button (top-right of tower)
self.deleteBtn = new Text2('✖', {
size: 40,
fill: "#fff"
});
self.deleteBtn.anchor.set(1, 0); // top-right
self.deleteBtn.x = 50;
self.deleteBtn.y = -50;
self.addChild(self.deleteBtn);
// Delete logic
self.deleteBtn.down = function (x, y, obj) {
// Remove from towers array
for (var i = towers.length - 1; i >= 0; i--) {
if (towers[i] === self) {
towers.splice(i, 1);
break;
}
}
// Remove from parent
self.destroy();
};
// Each tower instance manages its own stats and upgrades
self.range = 320;
self.damage = 5;
self.fireRate = 60; // frames per shot
self.cooldown = 0;
// For upgrade flash
self.isFlashing = false;
// Show range circle (for placement)
self.rangeCircle = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.15
});
self.rangeCircle.width = self.range * 2;
self.rangeCircle.height = self.range * 2;
self.rangeCircle.visible = false;
// Double-tap to boost this tower only
self.down = function (x, y, obj) {
var now = Date.now();
if (self._lastTapTime && now - self._lastTapTime < 400) {
// Double-tap detected: try to upgrade this tower
// Find color index for this tower
var colorIdx = 0;
if (self.children && self.children.length > 0) {
var tint = self.children[0].tint;
for (var i = 0; i < TOWER_SELECT_COLORS.length; i++) {
if (TOWER_SELECT_COLORS[i] === tint) {
colorIdx = i;
break;
}
}
}
// Calculate upgrade cost: base * current level
var upgradeCost = towerCurrentPrices[colorIdx] * self.level;
if (money >= upgradeCost) {
money -= upgradeCost;
// Increase all tower prices by tower price multiplier globally
for (var i = 0; i < towerCurrentPrices.length; i++) {
towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER);
}
// Actually upgrade tower
self.level++;
self.damage = Math.round(self.damage * TOWER_DAMAGE_UPGRADE_MULTIPLIER);
self.fireRate = Math.max(1, Math.round(self.fireRate / TOWER_SPEED_UPGRADE_MULTIPLIER));
// Update level label
self.levelLabel.setText(self.level + '');
if (typeof moneyTxt !== "undefined") {
moneyTxt.setText('Money: ' + money);
}
}
}
self._lastTapTime = now;
};
// Upgrade tower (for upgrade button or other triggers)
self.upgrade = function () {
if (self.level >= 3) {
return false;
}
self.level++;
if (self.levelLabel) {
self.levelLabel.setText(self.level + '');
}
// Find color index for this tower
var colorIdx = 0;
if (self.children && self.children.length > 0) {
var tint = self.children[0].tint;
for (var i = 0; i < TOWER_SELECT_COLORS.length; i++) {
if (TOWER_SELECT_COLORS[i] === tint) {
colorIdx = i;
break;
}
}
}
// Upgrade logic: scale up stats based on color
if (typeof TOWER_STATS !== "undefined" && TOWER_STATS[colorIdx]) {
self.damage += Math.round(TOWER_STATS[colorIdx].damage * 0.7);
self.range += Math.round(TOWER_STATS[colorIdx].range * 0.12);
self.fireRate = Math.max(20, self.fireRate - Math.round(TOWER_STATS[colorIdx].fireRate * 0.18));
} else {
self.damage += TOWER_DAMAGE_BLUE;
self.range += Math.round(TOWER_RANGE_STANDARD * 0.125);
self.fireRate = Math.max(30, self.fireRate - 10);
}
self.rangeCircle.width = self.range * 2;
self.rangeCircle.height = self.range * 2;
// Flash for upgrade
if (!self.isFlashing) {
self.isFlashing = true;
tween(towerAsset, {
tint: 0xffff00
}, {
duration: 120,
onFinish: function onFinish() {
tween(towerAsset, {
tint: 0x3498db
}, {
duration: 180,
onFinish: function onFinish() {
self.isFlashing = false;
}
});
}
});
}
return true;
};
// Tower attack logic
self.update = function () {
if (self.cooldown > 0) {
self.cooldown--;
return;
}
// Find nearest enemy in range
var nearest = null;
var minDist = 99999;
// Determine if this is a purple tower (Mor)
var isPurple = false;
if (self.children && self.children.length > 0 && self.children[0].tint === 0x8e44ad) {
isPurple = true;
}
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if ((isPurple || dist <= self.range) && dist < minDist) {
nearest = e;
minDist = dist;
}
}
if (nearest) {
// Shoot
var b = new Bullet();
b.x = self.x;
b.y = self.y;
b.target = nearest;
b.damage = self.damage;
b.speed = 24;
b.lastX = b.x;
b.lastY = b.y;
bullets.push(b);
game.addChild(b);
self.cooldown = self.fireRate;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Game Constants and Multipliers ---
// Tower stats constants
var TOWER_DAMAGE_BLUE = 5; // Mavi (blue) tower damage
var TOWER_DAMAGE_RED = 15; // Kırmızı (red) tower damage
var TOWER_DAMAGE_GREEN = 5; // Yeşil (green) tower damage
var TOWER_DAMAGE_YELLOW = 50; // Sarı (yellow) tower damage
var TOWER_DAMAGE_PURPLE = 15; // Mor (purple) tower damage
var TOWER_FIRE_RATE_BLUE = 60; // Mavi (blue) tower fire rate (frames per shot)
var TOWER_FIRE_RATE_RED = 60; // Kırmızı (red) tower fire rate
var TOWER_FIRE_RATE_GREEN = 20; // Yeşil (green) tower fire rate (3x faster than blue)
var TOWER_FIRE_RATE_YELLOW = 120; // Sarı (yellow) tower fire rate (2 seconds per shot)
var TOWER_FIRE_RATE_PURPLE = 120; // Mor (purple) tower fire rate (2 seconds per shot)
var TOWER_RANGE_STANDARD = 320; // Standard tower range
var TOWER_RANGE_GLOBAL = 2048; // Global range for purple tower
// Game progression multipliers
var ENEMY_BASE_HEALTH = 10; // Base health for enemies
var ENEMY_HEALTH_WAVE_MULTIPLIER = 1.25; // Health multiplier per wave
var TOWER_PRICE_INCREASE_MULTIPLIER = 1.25; // Tower price increase multiplier
// Tower upgrade multipliers
var TOWER_DAMAGE_UPGRADE_MULTIPLIER = 1.1; // Damage increase on upgrade
var TOWER_SPEED_UPGRADE_MULTIPLIER = 1.1; // Fire rate improvement on upgrade
// --- Grid system ---
// Set grid to 10x12 with margins, leave 1 row margin at bottom, and add 5 tower selection squares below grid
var GRID_COLS = 10;
var GRID_ROWS = 12;
var GRID_SIZE = 160;
// Calculate margins to center grid in 2048x2732, leaving 1 row margin at bottom for spacing
var GRID_TOTAL_WIDTH = GRID_COLS * GRID_SIZE;
var GRID_TOTAL_HEIGHT = GRID_ROWS * GRID_SIZE;
var GRID_MARGIN_X = Math.floor((2048 - GRID_TOTAL_WIDTH) / 2);
var GRID_MARGIN_Y = Math.floor((2732 - (GRID_TOTAL_HEIGHT + GRID_SIZE)) / 2); // +GRID_SIZE for 1 row margin at bottom
// Helper to snap to grid (with margin)
function snapToGrid(x, y) {
return {
x: GRID_MARGIN_X + Math.round((x - GRID_MARGIN_X) / GRID_SIZE) * GRID_SIZE,
y: GRID_MARGIN_Y + Math.round((y - GRID_MARGIN_Y) / GRID_SIZE) * GRID_SIZE
};
}
function generateSimplePath() {
var pathArr = [];
var visited = {}; // Ziyaret edilen hücreler ve geçiş yönleri: { h: boolean, v: boolean }
var yukariDonusYapildi = false; // YENİ: Yukarı dönüş yapılıp yapılmadığını takip eder
function cellKey(c, r) {
return c + "," + r;
}
var startCols = [];
for (var c = 1; c < GRID_COLS - 1; c++) {
startCols.push(c);
}
if (startCols.length === 0) {
return [snapToGrid(GRID_MARGIN_X, GRID_MARGIN_Y)];
}
var startCol = startCols[Math.floor(Math.random() * startCols.length)];
var curCol = startCol;
var curRow = 0;
pathArr.push(snapToGrid(GRID_MARGIN_X + curCol * GRID_SIZE, GRID_MARGIN_Y + curRow * GRID_SIZE));
var startKey = cellKey(curCol, curRow);
visited[startKey] = {
h: false,
v: true
}; // Başlangıçta aşağı varsayımı
curRow = 1;
if (GRID_ROWS < 2) {
return pathArr;
}
pathArr.push(snapToGrid(GRID_MARGIN_X + curCol * GRID_SIZE, GRID_MARGIN_Y + curRow * GRID_SIZE));
var firstStepKey = cellKey(curCol, curRow);
visited[firstStepKey] = visited[firstStepKey] || {
h: false,
v: false
};
visited[firstStepKey].v = true;
var endCols = [];
for (var c = 1; c < GRID_COLS - 1; c++) {
endCols.push(c);
}
if (endCols.length === 0) {
return pathArr;
}
var endCol = endCols[Math.floor(Math.random() * endCols.length)];
var endRow = GRID_ROWS - 1;
var preEndRow = GRID_ROWS - 2;
if (preEndRow < curRow) {
if (GRID_ROWS > 0) {
pathArr.push(snapToGrid(GRID_MARGIN_X + endCol * GRID_SIZE, GRID_MARGIN_Y + endRow * GRID_SIZE));
// visited[cellKey(endCol, endRow)] ...
}
return pathArr;
}
var lastDir = "down";
var straightCount = 1;
var maxRetries = GRID_COLS * GRID_ROWS * 15; // Biraz daha fazla deneme hakkı
var retries = 0;
while (!(curRow === preEndRow && curCol === endCol)) {
retries++;
if (retries > maxRetries) {
// console.warn("Path Gen: Max retries! Yukari Donus: " + yukariDonusYapildi);
break;
}
var potentialMoves = [];
// Potansiyel hamleleri oluştur (önceliklendirme olmadan tüm yönleri düşün)
if (curRow < preEndRow && lastDir !== "up") {
potentialMoves.push({
name: "down",
dx: 0,
dy: 1
});
}
if (curRow > 1 && lastDir !== "down") {
potentialMoves.push({
name: "up",
dx: 0,
dy: -1
});
}
if (curCol < GRID_COLS - 2 && lastDir !== "left") {
potentialMoves.push({
name: "right",
dx: 1,
dy: 0
});
}
if (curCol > 1 && lastDir !== "right") {
potentialMoves.push({
name: "left",
dx: -1,
dy: 0
});
}
// Eğer hiçbir potansiyel hamle yoksa (örn. 180 derece dönüş engeli nedeniyle), temel hamleleri tekrar ekle
if (potentialMoves.length === 0) {
if (curRow < preEndRow) {
potentialMoves.push({
name: "down",
dx: 0,
dy: 1
});
}
if (curRow > 1) {
potentialMoves.push({
name: "up",
dx: 0,
dy: -1
});
}
if (curCol < GRID_COLS - 2) {
potentialMoves.push({
name: "right",
dx: 1,
dy: 0
});
}
if (curCol > 1) {
potentialMoves.push({
name: "left",
dx: -1,
dy: 0
});
}
}
var validMovesRaw = [];
for (var i = 0; i < potentialMoves.length; i++) {
var move = potentialMoves[i];
var testCol = curCol + move.dx;
var testRow = curRow + move.dy;
var isHorizMove = move.dy === 0;
if (testCol < 1 || testCol > GRID_COLS - 2 || testRow < 1 || testRow > preEndRow) {
continue;
}
var vDataTarget = visited[cellKey(testCol, testRow)];
if (vDataTarget) {
if (isHorizMove && vDataTarget.h) {
continue;
}
if (!isHorizMove && vDataTarget.v) {
continue;
}
}
if (move.name === "up" && lastDir !== "up") {
// Sadece gerçek bir "yukarı dönüş" ise kontrol et
var upTurnSpacingOk = true;
for (var side = -1; side <= 1; side += 2) {
for (var dist = 1; dist <= 3; dist++) {
var checkC = testCol + side * dist;
if (checkC < 1 || checkC > GRID_COLS - 2) {
upTurnSpacingOk = false;
break;
}
var nKey = cellKey(checkC, testRow);
var vDataNeighbor = visited[nKey];
if (vDataNeighbor && (vDataNeighbor.h || vDataNeighbor.v)) {
upTurnSpacingOk = false;
break;
}
}
if (!upTurnSpacingOk) {
break;
}
}
if (!upTurnSpacingOk) {
continue;
}
}
var parallelSpacingOk = true;
if (isHorizMove) {
var nAboveKey = cellKey(testCol, testRow - 1);
var vAbove = visited[nAboveKey];
var nBelowKey = cellKey(testCol, testRow + 1);
var vBelow = visited[nBelowKey];
if (vAbove && vAbove.h && testRow - 1 !== curRow || vBelow && vBelow.h && testRow + 1 !== curRow) {
parallelSpacingOk = false;
}
} else {
var nLeftKey = cellKey(testCol - 1, testRow);
var vLeft = visited[nLeftKey];
var nRightKey = cellKey(testCol + 1, testRow);
var vRight = visited[nRightKey];
if (vLeft && vLeft.v && testCol - 1 !== curCol || vRight && vRight.v && testCol + 1 !== curCol) {
parallelSpacingOk = false;
}
}
if (!parallelSpacingOk) {
continue;
}
validMovesRaw.push(move);
}
if (validMovesRaw.length === 0) {
/*console.warn("No valid raw moves for " + curCol + "," + curRow);*/
break;
}
var currentActionableMoves = validMovesRaw;
if (straightCount >= 4) {
var nonStraightMoves = currentActionableMoves.filter(function (m) {
return m.name !== lastDir;
});
if (nonStraightMoves.length > 0) {
currentActionableMoves = nonStraightMoves;
}
}
if (currentActionableMoves.length === 0 && validMovesRaw.length > 0) {
currentActionableMoves = validMovesRaw;
}
if (currentActionableMoves.length === 0) {
/*console.warn("No actionable after straight count for " + curCol + "," + curRow);*/
break;
}
var selectedMove = null;
var potentialUpwardTurn = null;
if (!yukariDonusYapildi) {
currentActionableMoves.forEach(function (m) {
if (m.name === "up" && lastDir !== "up") {
potentialUpwardTurn = m; // Kurallara uyan bir yukarı dönüş bulundu
}
});
}
// Hamle Seçim Mantığı
if (potentialUpwardTurn) {
// Eğer yukarı dönüş yapılmadıysa ve mümkünse, onu önceliklendir
selectedMove = potentialUpwardTurn;
} else {
// Normal hamle seçim mantığı (düz gitme, hedefe yönelme vb.)
var moveInLastDir = null;
currentActionableMoves.forEach(function (m) {
if (m.name === lastDir) {
moveInLastDir = m;
}
});
var preferredMovesTowardsTarget = [];
currentActionableMoves.forEach(function (m) {
var candCol = curCol + m.dx;
var candRow = curRow + m.dy;
if (candCol === endCol && candRow === preEndRow) {
preferredMovesTowardsTarget.unshift(m);
} else {
var distOld = Math.abs(curCol - endCol) + Math.abs(curRow - preEndRow);
var distNew = Math.abs(candCol - endCol) + Math.abs(candRow - preEndRow);
if (distNew < distOld) {
preferredMovesTowardsTarget.push(m);
}
}
});
var straightAndPreferredTarget = null;
if (moveInLastDir && straightCount < 4) {
preferredMovesTowardsTarget.forEach(function (pm) {
if (pm.name === moveInLastDir.name) {
straightAndPreferredTarget = moveInLastDir;
}
});
}
if (straightAndPreferredTarget && Math.random() < 0.75) {
// %75 düz ve hedefe yönelik
selectedMove = straightAndPreferredTarget;
} else if (preferredMovesTowardsTarget.length > 0 && Math.random() < 0.80) {
// %80 hedefe yönelik
selectedMove = preferredMovesTowardsTarget[Math.floor(Math.random() * preferredMovesTowardsTarget.length)];
} else if (moveInLastDir && straightCount < 4 && Math.random() < 0.65) {
// %65 düz git
selectedMove = moveInLastDir;
} else if (currentActionableMoves.length > 0) {
// Son çare
selectedMove = currentActionableMoves[Math.floor(Math.random() * currentActionableMoves.length)];
} else {
break;
}
}
if (!selectedMove && currentActionableMoves.length > 0) {
// Eğer yukarıdaki mantıkla seçilemediyse (örn. potentialUpwardTurn vardı ama seçilmedi)
selectedMove = currentActionableMoves[Math.floor(Math.random() * currentActionableMoves.length)];
}
if (!selectedMove) {
/*console.warn("No move selected for " + curCol + "," + curRow);*/
break;
}
// Seçilen hamleyi uygula ve durumu güncelle
if (selectedMove.name === "up" && lastDir !== "up") {
yukariDonusYapildi = true;
}
var movedHorizontally = selectedMove.dy === 0;
if (selectedMove.name === lastDir) {
straightCount++;
} else {
straightCount = 1;
}
lastDir = selectedMove.name;
curCol += selectedMove.dx;
curRow += selectedMove.dy;
var newKey = cellKey(curCol, curRow);
visited[newKey] = visited[newKey] || {
h: false,
v: false
};
if (movedHorizontally) {
visited[newKey].h = true;
} else {
visited[newKey].v = true;
}
pathArr.push(snapToGrid(GRID_MARGIN_X + curCol * GRID_SIZE, GRID_MARGIN_Y + curRow * GRID_SIZE));
} // while sonu
// Eğer döngü bittiğinde hala yukarı dönüş yapılmadıysa ve yol çok kısaysa, bu bir sorun olabilir.
// Ancak algoritma, mümkün olan en kısa sürede yukarı dönüş yapmaya çalışacaktır.
// if (!yukariDonusYapildi && pathArr.length > (GRID_ROWS / 2)) console.log("Uyarı: Yol tamamlandı ancak yukarı dönüş yapılamadı.");
if (GRID_ROWS > 0) {
pathArr.push(snapToGrid(GRID_MARGIN_X + endCol * GRID_SIZE, GRID_MARGIN_Y + endRow * GRID_SIZE));
// visited[cellKey(endCol, endRow)] ...
}
return pathArr;
}
var path = generateSimplePath();
// --- Start Game Button Logic ---
var gameStarted = false;
var startBtn = new Text2('▶ Start Game', {
size: 120,
fill: 0x27AE60
});
startBtn.anchor.set(0.5, 0.5);
LK.gui.center.addChild(startBtn);
function setStartBtnVisible(visible) {
startBtn.visible = visible;
}
setStartBtnVisible(true);
startBtn.down = function (x, y, obj) {
if (!gameStarted) {
gameStarted = true;
setStartBtnVisible(false);
setNextWaveBtnVisible(true);
startWave(); // Start the first wave immediately
}
};
// Hide all gameplay UI until game starts
setNextWaveBtnVisible(false);
if (typeof kpTxt !== "undefined") {
kpTxt.visible = false;
}
if (typeof healthTxt !== "undefined") {
healthTxt.visible = false;
}
if (typeof waveTxt !== "undefined") {
waveTxt.visible = false;
}
if (typeof nextWaveBtn !== "undefined") {
nextWaveBtn.visible = false;
}
// autoNextWaveBtn is always visible; do not hide
// Show gameplay UI when game starts
function showGameplayUI() {
kpTxt.visible = true;
healthTxt.visible = true;
waveTxt.visible = true;
nextWaveBtn.visible = false;
nextWaveBtn.x = 0;
nextWaveBtn.y = kpTxt.y + kpTxt.height + 20;
// autoNextWaveBtn is always visible and positioned above waveTxt; do not reposition or show/hide here
kpTxt.setText('Points: ' + knowledgePoints);
healthTxt.setText('Base: ' + baseHealth);
waveTxt.setText('Wave: ' + currentWave + '/' + maxWaves);
}
var _origStartWave = startWave;
startWave = function startWave() {
if (!gameStarted) {
return;
}
showGameplayUI();
_origStartWave();
};
// Buildable tile logic removed for now
var buildTilePositions = [];
// Global game state
var enemies = [];
var towers = [];
var bullets = [];
// buildTiles removed for now
var baseHealth = 10;
var scoreo = 0; // Killed enemy count, used as score only
var money = 50; // Used for purchases, start with 50
var knowledgePoints = 30; // Deprecated, kept for UI compatibility
var currentWave = 1;
var maxWaves = 999999; // Effectively infinite waves
var waveInProgress = false;
var waveTimer = null;
var spawnIndex = 0;
var spawnDelay = 40; // frames between spawns
var enemiesToSpawn = 0;
var selectedTile = null;
// Tower price system
var towerBasePrices = [30, 50, 40, 60, 80]; // Starting price for each tower type (by power)
var towerCurrentPrices = towerBasePrices.slice(); // Current price for each type
// Asset initialization
// Draw grid background (light gray squares) with margins and 10x12 grid area
for (var row = 0; row < GRID_ROWS; row++) {
for (var col = 0; col < GRID_COLS; col++) {
var gridNode = LK.getAsset('buildTile', {
anchorX: 0.5,
anchorY: 0.5,
width: GRID_SIZE - 4,
height: GRID_SIZE - 4,
color: 0xf4f4f4,
shape: 'box',
alpha: 0.18
});
gridNode.x = GRID_MARGIN_X + col * GRID_SIZE;
gridNode.y = GRID_MARGIN_Y + row * GRID_SIZE;
game.addChild(gridNode);
}
}
// Draw 5 colored tower selection squares below the grid, centered horizontally
var TOWER_SELECT_COUNT = 5;
var TOWER_SELECT_SIZE = 140;
var TOWER_SELECT_SPACING = 60;
var TOWER_SELECT_COLORS = [0x3498db, 0xe74c3c, 0x2ecc40, 0xf1c40f, 0x8e44ad];
var TOWER_SELECT_LABELS = ['Mavi', 'Kırmızı', 'Yeşil', 'Sarı', 'Mor'];
// Tower stats for each color: {damage, range, fireRate}
var TOWER_STATS = [{
damage: TOWER_DAMAGE_BLUE,
range: TOWER_RANGE_STANDARD,
fireRate: TOWER_FIRE_RATE_BLUE
},
// Mavi
{
damage: TOWER_DAMAGE_RED,
range: TOWER_RANGE_STANDARD,
fireRate: TOWER_FIRE_RATE_RED
},
// Kırmızı
{
damage: TOWER_DAMAGE_GREEN,
range: TOWER_RANGE_STANDARD,
fireRate: TOWER_FIRE_RATE_GREEN
},
// Yeşil
{
damage: TOWER_DAMAGE_YELLOW,
range: TOWER_RANGE_STANDARD,
fireRate: TOWER_FIRE_RATE_YELLOW
},
// Sarı
{
damage: TOWER_DAMAGE_PURPLE,
range: TOWER_RANGE_GLOBAL,
fireRate: TOWER_FIRE_RATE_PURPLE
} // Mor (global range)
];
var towerSelectSquares = [];
var totalWidth = TOWER_SELECT_COUNT * TOWER_SELECT_SIZE + (TOWER_SELECT_COUNT - 1) * TOWER_SELECT_SPACING;
var startX = Math.floor((2048 - totalWidth) / 2) + TOWER_SELECT_SIZE / 2;
var selectY = GRID_MARGIN_Y + GRID_ROWS * GRID_SIZE + GRID_SIZE / 2;
for (var i = 0; i < TOWER_SELECT_COUNT; i++) {
var square = LK.getAsset('buildTile', {
anchorX: 0.5,
anchorY: 0.5,
width: TOWER_SELECT_SIZE,
height: TOWER_SELECT_SIZE,
color: TOWER_SELECT_COLORS[i],
shape: 'box',
alpha: 0.95
});
square.x = startX + i * (TOWER_SELECT_SIZE + TOWER_SELECT_SPACING);
square.y = selectY;
game.addChild(square);
// Add label for each tower color
var label = new Text2(TOWER_SELECT_LABELS[i], {
size: 48,
fill: "#fff"
});
label.anchor.set(0.5, 0);
label.x = square.x;
label.y = square.y + TOWER_SELECT_SIZE / 2 + 8;
game.addChild(label);
towerSelectSquares.push(square);
}
// Draw path (for visual reference)
for (var i = 0; i < path.length - 1; i++) {
var from = path[i];
var to = path[i + 1];
var dx = to.x - from.x;
var dy = to.y - from.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var steps = Math.floor(dist / 40);
for (var s = 0; s <= steps; s++) {
var t = s / steps;
var px = from.x + dx * t;
var py = from.y + dy * t;
var node = LK.getAsset('pathDot', {
anchorX: 0.5,
anchorY: 0.5,
width: 32,
height: 32,
color: 0x7f8c8d,
shape: 'ellipse'
});
node.x = px;
node.y = py;
game.addChild(node);
}
}
// Buildable tile objects removed for now
// --- Tower Selection Bar ---
var towerTypes = [];
var selectedTowerType = null;
var towerButtons = [];
var towerBtnY = 2400;
var towerBtnSpacing = 260;
var towerBtnStartX = 400;
var draggingTowerType = null;
var draggingTowerGhost = null;
// No tower selection buttons since normal tower is removed
// --- Drag-and-drop tower placement from colored squares ---
for (var i = 0; i < towerSelectSquares.length; i++) {
(function (colorIdx) {
var square = towerSelectSquares[colorIdx];
square.down = function (x, y, obj) {
// Clean up any previous ghost
if (draggingTowerGhost) {
if (draggingTowerGhost.rangeCircle) {
draggingTowerGhost.rangeCircle.destroy();
}
draggingTowerGhost.destroy();
draggingTowerGhost = null;
draggingTowerType = null;
}
draggingTowerType = colorIdx;
draggingTowerGhost = new Tower();
// Snap to grid for initial drag position
var gridX = Math.round((x - GRID_MARGIN_X) / GRID_SIZE);
var gridY = Math.round((y - GRID_MARGIN_Y) / GRID_SIZE);
var snapX = GRID_MARGIN_X + gridX * GRID_SIZE;
var snapY = GRID_MARGIN_Y + gridY * GRID_SIZE;
draggingTowerGhost.x = snapX;
draggingTowerGhost.y = snapY;
// Set color of ghost tower
if (draggingTowerGhost.children && draggingTowerGhost.children.length > 0) {
draggingTowerGhost.children[0].tint = TOWER_SELECT_COLORS[colorIdx];
}
// Set stats based on color for ghost
if (typeof TOWER_STATS !== "undefined" && TOWER_STATS[colorIdx]) {
draggingTowerGhost.damage = TOWER_STATS[colorIdx].damage;
draggingTowerGhost.range = TOWER_STATS[colorIdx].range;
draggingTowerGhost.fireRate = TOWER_STATS[colorIdx].fireRate;
if (draggingTowerGhost.rangeCircle) {
draggingTowerGhost.rangeCircle.width = draggingTowerGhost.range * 2;
draggingTowerGhost.rangeCircle.height = draggingTowerGhost.range * 2;
}
}
// Show range circle while dragging, and center it on the ghost
if (draggingTowerGhost.rangeCircle) {
draggingTowerGhost.rangeCircle.visible = true;
draggingTowerGhost.rangeCircle.x = 0;
draggingTowerGhost.rangeCircle.y = 0;
}
draggingTowerGhost.alpha = 0.7;
game.addChild(draggingTowerGhost);
};
})(i);
}
// Helper: check if a grid cell is on the path
function isCellOnPath(gridX, gridY) {
for (var i = 0; i < path.length; i++) {
var px = path[i].x;
var py = path[i].y;
var cellX = Math.round((px - GRID_MARGIN_X) / GRID_SIZE);
var cellY = Math.round((py - GRID_MARGIN_Y) / GRID_SIZE);
if (cellX === gridX && cellY === gridY) {
return true;
}
}
return false;
}
// Helper: check if a tower already exists at grid cell
function isTowerAtCell(gridX, gridY) {
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
var tx = Math.round((t.x - GRID_MARGIN_X) / GRID_SIZE);
var ty = Math.round((t.y - GRID_MARGIN_Y) / GRID_SIZE);
if (tx === gridX && ty === gridY) {
return true;
}
}
return false;
}
// Dragging ghost tower with finger/mouse
game.move = function (x, y, obj) {
if (draggingTowerGhost) {
// Snap ghost to grid for feedback
var gridX = Math.round((x - GRID_MARGIN_X) / GRID_SIZE);
var gridY = Math.round((y - GRID_MARGIN_Y) / GRID_SIZE);
var snapX = GRID_MARGIN_X + gridX * GRID_SIZE;
var snapY = GRID_MARGIN_Y + gridY * GRID_SIZE;
draggingTowerGhost.x = snapX;
draggingTowerGhost.y = snapY;
// Always center rangeCircle on the ghost (0,0 in local coordinates)
if (draggingTowerGhost.rangeCircle) {
draggingTowerGhost.rangeCircle.x = 0;
draggingTowerGhost.rangeCircle.y = 0;
}
// Visual feedback: tint red if invalid, normal if valid
var valid = true;
// Only allow inside grid
if (gridX < 0 || gridX >= GRID_COLS || gridY < 0 || gridY >= GRID_ROWS || isCellOnPath(gridX, gridY) || isTowerAtCell(gridX, gridY)) {
valid = false;
}
if (draggingTowerGhost.children && draggingTowerGhost.children.length > 0) {
draggingTowerGhost.children[0].tint = valid ? TOWER_SELECT_COLORS[draggingTowerType] : 0x888888;
}
draggingTowerGhost.alpha = valid ? 0.7 : 0.35;
}
};
// Place tower on grid on up
game.up = function (x, y, obj) {
if (draggingTowerGhost && draggingTowerType !== null) {
var gridX = Math.round((draggingTowerGhost.x - GRID_MARGIN_X) / GRID_SIZE);
var gridY = Math.round((draggingTowerGhost.y - GRID_MARGIN_Y) / GRID_SIZE);
var valid = true;
if (gridX < 0 || gridX >= GRID_COLS || gridY < 0 || gridY >= GRID_ROWS || isCellOnPath(gridX, gridY) || isTowerAtCell(gridX, gridY)) {
valid = false;
}
if (valid) {
// Place tower if enough money (use dynamic price)
var buildCost = towerCurrentPrices[draggingTowerType];
if (money >= buildCost) {
money -= buildCost;
// Increase all tower prices by tower price multiplier
for (var i = 0; i < towerCurrentPrices.length; i++) {
towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER);
}
var tower = new Tower();
tower.x = GRID_MARGIN_X + gridX * GRID_SIZE;
tower.y = GRID_MARGIN_Y + gridY * GRID_SIZE;
// Set color
if (tower.children && tower.children.length > 0) {
tower.children[0].tint = TOWER_SELECT_COLORS[draggingTowerType];
}
// Set stats based on color
if (typeof TOWER_STATS !== "undefined" && TOWER_STATS[draggingTowerType]) {
tower.damage = TOWER_STATS[draggingTowerType].damage;
tower.range = TOWER_STATS[draggingTowerType].range;
tower.fireRate = TOWER_STATS[draggingTowerType].fireRate;
if (tower.rangeCircle) {
tower.rangeCircle.width = tower.range * 2;
tower.rangeCircle.height = tower.range * 2;
}
}
towers.push(tower);
game.addChild(tower);
moneyTxt.setText('Money: ' + money);
}
}
// Clean up ghost
if (draggingTowerGhost.rangeCircle) {
draggingTowerGhost.rangeCircle.destroy();
}
draggingTowerGhost.destroy();
draggingTowerGhost = null;
draggingTowerType = null;
return;
}
// If not placing, just clear ghost
if (draggingTowerGhost) {
if (draggingTowerGhost.rangeCircle) {
draggingTowerGhost.rangeCircle.destroy();
}
draggingTowerGhost.destroy();
draggingTowerGhost = null;
draggingTowerType = null;
}
};
// Base indicator (ellipse, red)
var baseNode = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5,
width: 160,
height: 160,
color: 0xe74c3c,
shape: 'ellipse'
});
baseNode.x = path[path.length - 1].x;
baseNode.y = path[path.length - 1].y;
game.addChild(baseNode);
// GUI: Points (KP) - Center top, smaller size, renamed to Points
var kpTxt = new Text2('Score: ' + scoreo, {
size: 48,
fill: 0x2ecc40 // bright green, not transparent
});
kpTxt.anchor.set(0.5, 0); // center aligned, top
kpTxt.x = 0;
kpTxt.y = 10;
LK.gui.top.addChild(kpTxt);
// GUI: Money - Bottom right
var moneyTxt = new Text2('Money: ' + money, {
size: 48,
fill: 0xf1c40f
});
moneyTxt.anchor.set(1, 1); // right aligned, bottom
moneyTxt.x = -40; // 40px from right edge
moneyTxt.y = -40; // 40px from bottom edge
LK.gui.bottomRight.addChild(moneyTxt);
// GUI: Base Health - Top right, smaller size
var healthTxt = new Text2('Base: ' + baseHealth, {
size: 48,
fill: 0xE74C3C
});
healthTxt.anchor.set(1, 0); // right aligned, top
healthTxt.x = -40; // right margin
healthTxt.y = 10;
LK.gui.topRight.addChild(healthTxt);
// GUI: Wave - Bottom left, smaller size
var waveTxt = new Text2('Wave: ' + currentWave + '/' + maxWaves, {
size: 48,
fill: 0x2980B9
});
waveTxt.anchor.set(0, 1); // left aligned, bottom
waveTxt.x = 40;
waveTxt.y = -40;
// Auto-next wave toggle (now above waveTxt, same size)
var autoNextWave = false;
var autoNextWaveBtn = new Text2('Auto: OFF', {
size: 48,
fill: 0x2980B9
});
autoNextWaveBtn.anchor.set(0, 1);
autoNextWaveBtn.x = 40;
autoNextWaveBtn.y = waveTxt.y - waveTxt.height - 8; // 8px gap above waveTxt
LK.gui.bottomLeft.addChild(autoNextWaveBtn);
LK.gui.bottomLeft.addChild(waveTxt);
autoNextWaveBtn.visible = true;
autoNextWaveBtn.down = function (x, y, obj) {
autoNextWave = !autoNextWave;
autoNextWaveBtn.setText('Auto: ' + (autoNextWave ? 'ON' : 'OFF'));
};
// Show/hide next wave button
function setNextWaveBtnVisible(visible) {
// Defensive: Only set visible if nextWaveBtn is defined and has a visible property
if (typeof nextWaveBtn !== "undefined" && nextWaveBtn && typeof nextWaveBtn.visible !== "undefined") {
nextWaveBtn.visible = visible;
}
}
setNextWaveBtnVisible(false);
// Start next wave
function startWave() {
if (waveInProgress || currentWave > maxWaves) {
return;
}
waveInProgress = true;
setNextWaveBtnVisible(false);
enemiesToSpawn = 6 + currentWave * 2;
spawnIndex = 0;
}
// Next wave button (centered below Points text, always present but hidden when not needed)
var nextWaveBtn = new Text2('Next Wave', {
size: 64,
fill: 0x2980B9
});
nextWaveBtn.anchor.set(0.5, 0);
nextWaveBtn.x = 0;
nextWaveBtn.y = kpTxt.y + kpTxt.height + 20;
LK.gui.top.addChild(nextWaveBtn);
nextWaveBtn.visible = false;
// Next wave button event
nextWaveBtn.down = function (x, y, obj) {
if (!waveInProgress && currentWave <= maxWaves) {
startWave();
}
};
// Build/upgrade tower on tile
function tryBuildOrUpgrade(tile) {
// All upgrades and purchases use only money
if (tile.occupied) {
// Try upgrade
var colorIdx = 0;
if (tile.tower.children && tile.tower.children.length > 0) {
var tint = tile.tower.children[0].tint;
for (var i = 0; i < TOWER_SELECT_COLORS.length; i++) {
if (TOWER_SELECT_COLORS[i] === tint) {
colorIdx = i;
break;
}
}
}
var upgradeCost = towerCurrentPrices[colorIdx] * tile.tower.level;
if (tile.tower.level < 3) {
if (money >= upgradeCost) {
money -= upgradeCost;
// Increase all tower prices by tower price multiplier globally
for (var i = 0; i < towerCurrentPrices.length; i++) {
towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER);
}
tile.tower.upgrade();
moneyTxt.setText('Money: ' + money);
}
}
} else {
// Build new tower
var buildCost = towerCurrentPrices[0];
if (money >= buildCost) {
money -= buildCost;
// Increase all tower prices by tower price multiplier globally
for (var i = 0; i < towerCurrentPrices.length; i++) {
towerCurrentPrices[i] = Math.round(towerCurrentPrices[i] * TOWER_PRICE_INCREASE_MULTIPLIER);
}
var tower = new Tower();
tower.x = tile.x;
tower.y = tile.y;
towers.push(tower);
game.addChild(tower);
tile.occupied = true;
tile.tower = tower;
moneyTxt.setText('Money: ' + money);
}
}
// Score is only for kill count, handled in enemy kill logic
}
// Highlight build tiles on touch
game.down = function (x, y, obj) {
// No tap-to-place logic; only drag-and-drop is supported for tower placement
};
// Remove highlight/range on up
// (handled by drag-and-drop up logic above)
// Main game update
game.update = function () {
// Spawn enemies for wave
if (waveInProgress && enemiesToSpawn > 0 && LK.ticks % spawnDelay === 0) {
var enemy = new Enemy();
enemy.x = path[0].x;
enemy.y = path[0].y;
enemy.pathIndex = 0;
enemy.pathProgress = 0;
enemies.push(enemy);
game.addChild(enemy);
enemiesToSpawn--;
}
// Update enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
e.update();
if (e.health <= 0) {
// Enemy defeated
scoreo += 1;
// Give money: 10 * currentWave
var reward = currentWave * 10;
money += reward;
kpTxt.setText('Score: ' + scoreo);
moneyTxt.setText('Money: ' + money);
e.destroy();
enemies.splice(i, 1);
} else if (e.reachedBase) {
// Enemy reached base
baseHealth--;
healthTxt.setText('Base: ' + baseHealth);
e.destroy();
enemies.splice(i, 1);
healthTxt.setText('Base: ' + baseHealth);
LK.effects.flashScreen(0xe74c3c, 400);
if (baseHealth <= 0) {
LK.showGameOver();
return;
}
}
}
// Update towers
for (var i = 0; i < towers.length; i++) {
towers[i].update();
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
b.update();
if (b.destroyed) {
b.destroy();
bullets.splice(i, 1);
}
}
// End wave if all enemies defeated and none left to spawn
if (waveInProgress && enemies.length === 0 && enemiesToSpawn === 0) {
waveInProgress = false;
currentWave++;
// No win condition for infinite waves
waveTxt.setText('Wave: ' + currentWave + '/' + maxWaves);
// nextWaveBtn is repositioned, but autoNextWaveBtn is always visible and fixed above waveTxt
nextWaveBtn.x = 0;
nextWaveBtn.y = kpTxt.y + kpTxt.height + 20;
// autoNextWaveBtn position is fixed above waveTxt; do not reposition or show/hide here
if (autoNextWave) {
setNextWaveBtnVisible(false);
// Start next wave automatically after a short delay for feedback
LK.setTimeout(function () {
if (!waveInProgress && currentWave <= maxWaves) {
startWave();
}
}, 700);
} else {
setNextWaveBtnVisible(true);
}
}
};