Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Building = Container.expand(function (id, size) { var self = Container.call(this); self.id = id || 'default'; self.size = size || { w: 2, h: 2 }; var assetId = 'building_' + self.id; var baseGraphics = self.attachAsset(assetId, { anchorX: 0, anchorY: 0 }); baseGraphics.width = CELL_SIZE * self.size.w; baseGraphics.height = CELL_SIZE * self.size.h; baseGraphics.tint = 0xFFFFFF; baseGraphics.alpha = 1; self.down = function () { if (isMenuTransitioning) { return; } game.children.forEach(function (child) { if (child instanceof BuildingInfoMenu) { hideBuildingInfoMenu(child); } }); if (selectedBuilding === self) { selectedBuilding = null; return; } selectedBuilding = self; var menu = game.addChild(new BuildingInfoMenu(self)); menu.x = 2048 / 2; isMenuTransitioning = true; tween(menu, { y: 2732 - 250 }, { duration: 200, easing: tween.backOut, onFinish: function onFinish() { isMenuTransitioning = false; } }); }; return self; }); var BuildingInfoMenu = Container.expand(function (building) { var self = Container.call(this); self.building = building; self.y = 2732 + 225; var buildingData = BUILDING_DATA[building.id]; 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 buildingNameText = new Text2(buildingData.name, { size: 80, fill: 0xFFD700, weight: 800 }); buildingNameText.anchor.set(0.5, 0); buildingNameText.x = 0; buildingNameText.y = -180; self.addChild(buildingNameText); var descriptionText = new Text2(buildingData.description, { size: 60, fill: 0xFFFFFF, weight: 400, wordWrap: true, wordWrapWidth: 1600, align: 'center' }); descriptionText.anchor.set(0.5, 0.5); descriptionText.x = 0; descriptionText.y = 20; self.addChild(descriptionText); var closeButton = new Container(); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 90, height: 90, 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; self.addChild(closeButton); closeButton.down = function () { hideBuildingInfoMenu(self); }; var originalDestroy = self.destroy; self.destroy = function () { if (selectedBuilding === self.building) { selectedBuilding = null; } originalDestroy.call(self); }; return self; }); /**** * Classes ****/ var Bullet = Container.expand(function (startX, startY, targetEnemy, props) { var self = Container.call(this); self.targetEnemy = targetEnemy; self.props = props; self.x = startX; self.y = startY; self.isAnimated = Array.isArray(self.props.graphic); self.allFrames = []; if (self.isAnimated) { var frameNames = self.props.graphic; for (var i = 0; i < frameNames.length; i++) { var frameSprite = self.attachAsset(frameNames[i], { anchorX: 0.5, anchorY: 0.5 }); frameSprite.visible = false; self.allFrames.push(frameSprite); } self.currentFrame = 0; self.frameDuration = 4; self.frameTicker = 0; if (self.allFrames.length > 0) { self.allFrames[self.currentFrame].visible = true; } } else { var bulletGraphics = self.attachAsset(self.props.graphic || 'bullet', { anchorX: 0.5, anchorY: 0.5 }); if (self.props.bulletType === 'chain') { bulletGraphics.visible = false; } self.allFrames.push(bulletGraphics); } if (self.props.bulletType === 'splash') { self.allFrames[0].tint = 0xFF8C00; } if (self.props.bulletType === 'slow') { self.allFrames.forEach(function (frame) { frame.tint = 0xADD8E6; }); } self.update = function () { if (!self.targetEnemy || !self.targetEnemy.parent || self.targetEnemy.health <= 0) { self.destroy(); return; } var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); var distance = Math.sqrt(dx * dx + dy * dy); if (self.isAnimated) { self.frameTicker++; if (self.frameTicker >= self.frameDuration) { self.frameTicker = 0; self.allFrames[self.currentFrame].visible = false; self.currentFrame = (self.currentFrame + 1) % self.allFrames.length; self.allFrames[self.currentFrame].visible = true; } } self.allFrames.forEach(function (frame) { if (self.props.graphic === 'projectile_fireball') { frame.rotation += 0.2; } else { frame.rotation = angle; } }); if (distance < self.props.bulletSpeed) { self.targetEnemy.takeDamage(self.props.damage); switch (self.props.bulletType) { case 'slow': if (Math.random() < self.props.slowChance) { if (self.targetEnemy.slowDuration <= 0) { self.targetEnemy.originalTint = self.targetEnemy.children[0].tint; } self.targetEnemy.children[0].tint = 0xADD8E6; self.targetEnemy.slowDuration = self.props.slowDuration; self.targetEnemy.slowFactor = self.props.slowAmount; } break; case 'splash': spellEffectLayer.addChild(new Explosion(self.x, self.y)); enemies.forEach(function (enemy) { if (enemy !== self.targetEnemy) { var distToSplash = Math.sqrt(Math.pow(self.targetEnemy.x - enemy.x, 2) + Math.pow(self.targetEnemy.y - enemy.y, 2)); if (distToSplash < self.props.splashRadius) { enemy.takeDamage(self.props.splashDamage); } } }); break; case 'chain': spellEffectLayer.addChild(new LightningStrikeEffect(self.x, self.y)); if (Math.random() < self.props.chainChance) { var chainedTargets = [self.targetEnemy]; var lastTarget = self.targetEnemy; var damage = self.props.damage; for (var i = 0; i < self.props.chainTargets; i++) { damage -= self.props.chainDamageFalloff; if (damage <= 0) { break; } var nextTarget = null; var minChainDist = Infinity; enemies.forEach(function (enemy) { if (!chainedTargets.includes(enemy)) { var distToChain = Math.sqrt(Math.pow(lastTarget.x - enemy.x, 2) + Math.pow(lastTarget.y - enemy.y, 2)); if (distToChain < self.props.range / 2 && distToChain < minChainDist) { minChainDist = distToChain; nextTarget = enemy; } } }); if (nextTarget) { nextTarget.takeDamage(damage); spellEffectLayer.addChild(new ChainLightningVisual(lastTarget, nextTarget)); chainedTargets.push(nextTarget); lastTarget = nextTarget; } else { break; } } } break; } self.destroy(); } else { self.x += Math.cos(angle) * self.props.bulletSpeed; self.y += Math.sin(angle) * self.props.bulletSpeed; } }; return self; }); var ChainLightningVisual = Container.expand(function (sourceEnemy, targetEnemy) { var self = Container.call(this); var dx = targetEnemy.x - sourceEnemy.x; var dy = targetEnemy.y - sourceEnemy.y; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); self.x = sourceEnemy.x; self.y = sourceEnemy.y; self.rotation = angle; var frameNames = []; for (var i = 1; i <= 4; i++) { frameNames.push('chain_lightning_' + i); } var allFrames = []; for (var i = 0; i < frameNames.length; i++) { var frameSprite = self.attachAsset(frameNames[i], { anchorX: 0, anchorY: 0.5, width: distance, height: 80 }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 6; var frameTicker = 0; if (allFrames.length > 0) { allFrames[currentFrame].visible = true; } self.update = function () { frameTicker++; if (frameTicker >= frameDuration) { frameTicker = 0; if (allFrames[currentFrame]) { allFrames[currentFrame].visible = false; } currentFrame++; if (currentFrame >= allFrames.length) { self.destroy(); } else { if (allFrames[currentFrame]) { allFrames[currentFrame].visible = true; } } } }; return self; }); var CurseEffect = Container.expand(function (x, y, spellData) { var self = Container.call(this); self.x = x; self.y = y; var radius = spellData.radius; var radiusSq = radius * radius; var duration = spellData.duration; var isFadingOut = false; var effectGraphics = self.attachAsset('curse_effect_circle', { anchorX: 0.5, anchorY: 0.5, width: radius * 2, height: radius * 2 }); effectGraphics.alpha = 0; tween(effectGraphics, { alpha: 0.8 }, { duration: 500 }); self.update = function () { if (isFadingOut) { return; } duration--; if (duration <= 0) { isFadingOut = true; tween(self, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { self.destroy(); } }); return; } effectGraphics.rotation -= 0.005; enemies.forEach(function (enemy) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; if (dx * dx + dy * dy < radiusSq) { enemy.isCursed = true; enemy.curseTimer = 120; enemy.damageTakenMultiplier = spellData.damageMultiplier; enemy.slowFactor = Math.min(enemy.slowFactor, spellData.slowFactor); } }); }; 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: case 2: { 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 1: { self.removeArrows(); cellGraphics.tint = 0xaaaaaa; numberLabel.visible = false; break; } case 3: { self.removeArrows(); cellGraphics.tint = 0x008800; numberLabel.visible = false; break; } } numberLabel.setText(Math.floor(data.score / 1000) / 10); }; }); 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; 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; }); var Enemy = Container.expand(function (type, spawnerId) { var self = Container.call(this); self.type = type || 'normal'; self.spawnerId = spawnerId; self.laneIndex = 0; self.speed = 0.01; self.originalSpeed = self.speed; self.slowDuration = 0; self.slowFactor = 1; self.isCursed = false; self.curseTimer = 0; self.damageTakenMultiplier = 1; self.isBurning = false; self.burnDuration = 0; self.burnDamage = 0; self.lastBurnTick = 0; self.isStunned = false; self.stunTimer = 0; self.originalTint = 0xFFFFFF; self.cellX = 0; self.cellY = 0; self.currentCellX = 0; self.currentCellY = 0; self.currentTarget = undefined; self.maxHealth = 78; self.health = self.maxHealth; self.combatSpeed = self.speed * 75; self.bulletsTargetingThis = []; self.waveNumber = currentWave; self.isFlying = false; self.isImmune = false; self.isBoss = false; self.isRanged = false; self.manaCategory = 'strong'; self.damage = 10; self.attackSpeed = 120; self.lastAttack = 0; self.range = 1.5 * CELL_SIZE; self.targetUnit = null; self.goldValue = 10; switch (self.type) { case 'shield': self.maxHealth = 94; self.damage = 3; self.manaCategory = 'strong'; self.goldValue = 36; break; case 'rider': self.maxHealth = 78; self.damage = 6; self.speed *= 1.5; self.manaCategory = 'strong'; self.goldValue = 59; break; case 'chieftain': self.maxHealth = 524; self.damage = 10; self.isBoss = true; self.manaCategory = 'boss'; self.goldValue = 304; break; case 'archer': self.maxHealth = 31; self.damage = 7; self.attackSpeed = 48; self.isRanged = true; self.range = 6 * CELL_SIZE; self.manaCategory = 'strong'; self.goldValue = 34; break; case 'normal': default: self.maxHealth = 62; self.damage = 4; self.manaCategory = 'weak'; self.goldValue = 19; break; } self.originalSpeed = self.speed; self.health = self.maxHealth; var assetId = 'orc_' + self.type; var enemyGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.originalTint = enemyGraphics.tint; if (self.isBoss) { enemyGraphics.scaleX = 1.8; enemyGraphics.scaleY = 1.8; } 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.takeDamage = function (amount) { var finalDamage = amount * self.damageTakenMultiplier; self.health -= finalDamage; if (self.health < 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; }; self.findTarget = function () { if (self.isFlying) { return null; } var closestUnit = null; var minDistance = self.range * self.range; for (var i = 0; i < friendlyUnits.length; i++) { var unit = friendlyUnits[i]; var dx = unit.x - self.x; var dy = unit.y - self.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistance) { minDistance = distanceSq; closestUnit = unit; } } return closestUnit; }; self.fireProjectile = function () { var bulletProps = { damage: self.damage, bulletSpeed: 10, graphic: 'arrow_orc_archer' }; var bullet = new Bullet(self.x, self.y, self.targetUnit, bulletProps); gateContainer.addChild(bullet); bullets.push(bullet); }; self.attack = function () { if (!self.targetUnit || self.targetUnit.health <= 0) { return; } if (self.isRanged) { self.fireProjectile(); return; } self.targetUnit.takeDamage(self.damage); var angle = Math.atan2(self.targetUnit.y - self.y, self.targetUnit.x - self.x); var recoilDistance = 5; var originalX = self.targetUnit.x; var originalY = self.targetUnit.y; tween(self.targetUnit, { x: originalX + Math.cos(angle) * recoilDistance, y: originalY + Math.sin(angle) * recoilDistance }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() { if (self.targetUnit) { tween(self.targetUnit, { x: originalX, y: originalY }, { duration: 120, easing: tween.easeIn }); } } }); var unitGraphics = self.targetUnit.children[0]; if (unitGraphics) { var originalTint = unitGraphics.tint; unitGraphics.tint = 0xFF0000; LK.setTimeout(function () { if (unitGraphics && !unitGraphics.destroyed) { unitGraphics.tint = originalTint; } }, 100); } createHitSparks(self.targetUnit.x, self.targetUnit.y); }; self.update = function () { if (self.isStunned) { self.stunTimer--; if (self.stunTimer <= 0) { self.isStunned = false; } return false; } self.speed = self.originalSpeed; if (self.slowDuration > 0) { self.slowDuration--; self.speed *= self.slowFactor; if (self.slowDuration <= 0) { self.slowFactor = 1; enemyGraphics.tint = self.originalTint; } } if (self.isCursed) { self.curseTimer--; if (self.curseTimer <= 0) { self.isCursed = false; self.damageTakenMultiplier = 1; } else { self.speed *= self.slowFactor; } } if (self.isBurning) { if (LK.ticks - self.lastBurnTick >= 60) { self.takeDamage(self.burnDamage); self.lastBurnTick = LK.ticks; } self.burnDuration--; if (self.burnDuration <= 0) { self.isBurning = false; } } if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; return false; } self.targetUnit = self.findTarget(); if (self.targetUnit && self.targetUnit.health > 0 && !self.isFlying) { var dx = self.targetUnit.x - self.x; var dy = self.targetUnit.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); if (distance < self.range) { if (LK.ticks - self.lastAttack >= self.attackSpeed) { self.attack(); self.lastAttack = LK.ticks; } } else { var moveX = dx; var moveY = dy; var separationX = 0; var separationY = 0; var separationRadius = 40; enemies.forEach(function (otherEnemy) { if (otherEnemy === self || otherEnemy.isFlying) { return; } var otherDx = self.x - otherEnemy.x; var otherDy = self.y - otherEnemy.y; var otherDist = Math.sqrt(otherDx * otherDx + otherDy * otherDy); if (otherDist > 0 && otherDist < separationRadius) { separationX += otherDx / (otherDist * otherDist); separationY += otherDy / (otherDist * otherDist); } }); var separationWeight = 15; moveX += separationX * separationWeight; moveY += separationY * separationWeight; var moveMagnitude = Math.sqrt(moveX * moveX + moveY * moveY); if (moveMagnitude > 0) { self.x += moveX / moveMagnitude * self.combatSpeed; self.y += moveY / moveMagnitude * self.combatSpeed; } } } else { var currentCell = grid.getCell(Math.round(self.currentCellX), Math.round(self.currentCellY)); if (currentCell && currentCell.targets[self.spawnerId]) { if (grid.updateEnemy(self)) { return true; } } else { var recoveryTarget = null; var minRecoveryDist = Infinity; for (var i = 0; i < grid.cells.length; i++) { for (var j = 0; j < grid.cells[i].length; j++) { var cell = grid.cells[i][j]; if (cell.targets[self.spawnerId]) { var distSq = Math.pow(self.x - (grid.x + i * CELL_SIZE), 2) + Math.pow(self.y - (grid.y + j * CELL_SIZE), 2); if (distSq < minRecoveryDist) { minRecoveryDist = distSq; recoveryTarget = cell; } } } } if (recoveryTarget) { var angle = Math.atan2(grid.y + recoveryTarget.y * CELL_SIZE - self.y, grid.x + recoveryTarget.x * CELL_SIZE - self.x); self.x += Math.cos(angle) * self.combatSpeed; self.y += Math.sin(angle) * self.combatSpeed; } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; return false; }; return self; }); var Explosion = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; var allFrames = []; for (var i = 1; i <= 8; i++) { var frameSprite = self.attachAsset('explosion_fire_' + i, { anchorX: 0.5, anchorY: 0.5 }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 5; var frameTicker = 0; if (allFrames.length > 0) { allFrames[currentFrame].visible = true; } self.update = function () { if (allFrames.length === 0) { self.destroy(); return; } frameTicker++; if (frameTicker >= frameDuration) { frameTicker = 0; allFrames[currentFrame].visible = false; currentFrame++; if (currentFrame >= allFrames.length) { self.destroy(); } else { allFrames[currentFrame].visible = true; } } }; return self; }); var FireWall = Container.expand(function (x, y, spellData) { var self = Container.call(this); self.x = x; self.y = y; self.spellData = spellData; self.duration = self.spellData.duration; self.radiusSq = self.spellData.radius * self.spellData.radius; var isFadingOut = false; var frameNames = []; for (var i = 1; i <= 13; i++) { frameNames.push('fire_wall_' + i); } var allFrames = []; var frameSize = self.spellData.radius * 2; for (var i = 0; i < frameNames.length; i++) { var frameSprite = self.attachAsset(frameNames[i], { anchorX: 0.5, anchorY: 0.5, width: frameSize, height: frameSize }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 6; var frameTicker = 0; if (allFrames.length > 0) { allFrames[currentFrame].visible = true; } self.update = function () { if (isFadingOut) { return; } self.duration--; if (self.duration <= 0) { isFadingOut = true; var spellIndex = activeSpells.indexOf(self); if (spellIndex !== -1) { activeSpells.splice(spellIndex, 1); } tween(self, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { self.destroy(); } }); return; } frameTicker++; if (frameTicker >= frameDuration) { frameTicker = 0; if (allFrames[currentFrame]) { allFrames[currentFrame].visible = false; } currentFrame = (currentFrame + 1) % allFrames.length; if (allFrames[currentFrame]) { allFrames[currentFrame].visible = true; } } enemies.forEach(function (enemy) { if (enemy.isBurning) { return; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; if (dx * dx + dy * dy < self.radiusSq) { enemy.isBurning = true; enemy.burnDuration = self.spellData.dotDuration; enemy.burnDamage = self.spellData.dotDamage; } }); }; return self; }); var FriendlyUnit = Container.expand(function (startX, startY, unitStats) { var self = Container.call(this); self.unitType = unitStats.id || 'swordsman'; Object.assign(self, unitStats); self.x = startX; self.y = startY; self.health = self.maxHealth; self.lastAttack = 0; self.targetEnemy = null; var assetId = 'unit_' + self.unitType; var unitGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); 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 = -unitGraphics.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.takeDamage = function (amount) { self.health -= amount; if (self.health < 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; }; self.findClosestEnemy = function () { var closestEnemy = null; var minDistanceSq = self.detectionRange * self.detectionRange; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.isFlying && !self.isRanged) { continue; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistanceSq) { minDistanceSq = distanceSq; closestEnemy = enemy; } } return closestEnemy; }; self.fireProjectile = function () { var bulletProps = { damage: self.damage, bulletSpeed: 12, graphic: 'arrow_unit_archer' }; var bullet = new Bullet(self.x, self.y, self.targetEnemy, bulletProps); gateContainer.addChild(bullet); bullets.push(bullet); }; self.attack = function () { if (!self.targetEnemy || self.targetEnemy.health <= 0) { return; } if (self.isRanged) { self.fireProjectile(); return; } self.targetEnemy.takeDamage(self.damage); var angle = Math.atan2(self.targetEnemy.y - self.y, self.targetEnemy.x - self.x); var recoilDistance = 5; var originalX = self.targetEnemy.x; var originalY = self.targetEnemy.y; tween(self.targetEnemy, { x: originalX + Math.cos(angle) * recoilDistance, y: originalY + Math.sin(angle) * recoilDistance }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() { if (self.targetEnemy) { tween(self.targetEnemy, { x: originalX, y: originalY }, { duration: 120, easing: tween.easeIn }); } } }); var enemyGraphics = self.targetEnemy.children[0]; if (enemyGraphics) { var originalTint = enemyGraphics.tint; enemyGraphics.tint = 0xFF0000; LK.setTimeout(function () { if (enemyGraphics && !enemyGraphics.destroyed) { enemyGraphics.tint = originalTint; } }, 100); } createHitSparks(self.targetEnemy.x, self.targetEnemy.y); }; self.die = function () { peopleRegenQueue.push(LK.ticks); var unitIndex = friendlyUnits.indexOf(self); if (unitIndex !== -1) { friendlyUnits.splice(unitIndex, 1); } self.destroy(); }; self.update = function () { if (self.health <= 0) { self.die(); return; } self.healthBar.width = self.health / self.maxHealth * 70; if (!self.targetEnemy || self.targetEnemy.health <= 0 || !self.targetEnemy.parent) { self.targetEnemy = self.findClosestEnemy(); } if (self.targetEnemy) { 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.range) { if (LK.ticks - self.lastAttack >= self.attackSpeed) { self.attack(); self.lastAttack = LK.ticks; } return; } else if (distance < self.detectionRange) { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; return; } else { self.targetEnemy = null; } } if (enemies.length === 0) { return; } var gridX = Math.floor((self.x - grid.x) / CELL_SIZE); var gridY = Math.floor((self.y - grid.y) / CELL_SIZE); var currentCell = grid.getCell(gridX, gridY); var targetCell = null; if (currentCell && currentCell.aggressionTargets && currentCell.aggressionTargets.length > 0) { targetCell = currentCell.aggressionTargets[0]; } var moveX = 0; var moveY = 0; if (targetCell) { var targetX = grid.x + targetCell.x * CELL_SIZE + CELL_SIZE / 2; var targetY = grid.y + targetCell.y * CELL_SIZE + CELL_SIZE / 2; var angle = Math.atan2(targetY - self.y, targetX - self.x); moveX += Math.cos(angle); moveY += Math.sin(angle); } else { var recoveryTarget = null; var startX = Math.round((self.x - grid.x) / CELL_SIZE); var startY = Math.round((self.y - grid.y) / CELL_SIZE); for (var radius = 0; radius < 5 && !recoveryTarget; radius++) { for (var i = -radius; i <= radius; i++) { for (var j = -radius; j <= radius; j++) { if (Math.abs(i) !== radius && Math.abs(j) !== radius) { continue; } var checkCell = grid.getCell(startX + i, startY + j); if (checkCell && checkCell.type === 0) { recoveryTarget = checkCell; break; } } if (recoveryTarget) { break; } } } if (recoveryTarget) { var recoveryTargetX = grid.x + recoveryTarget.x * CELL_SIZE + CELL_SIZE / 2; var recoveryTargetY = grid.y + recoveryTarget.y * CELL_SIZE + CELL_SIZE / 2; var angle = Math.atan2(recoveryTargetY - self.y, recoveryTargetX - self.x); moveX += Math.cos(angle); moveY += Math.sin(angle); } else { moveY -= 1; } } var separationX = 0; var separationY = 0; var separationRadius = 40; for (var i = 0; i < friendlyUnits.length; i++) { var otherUnit = friendlyUnits[i]; if (otherUnit === self) { continue; } var otherDx = self.x - otherUnit.x; var otherDy = self.y - otherUnit.y; var otherDist = Math.sqrt(otherDx * otherDx + otherDy * otherDy); if (otherDist > 0 && otherDist < separationRadius) { separationX += otherDx / (otherDist * otherDist); separationY += otherDy / (otherDist * otherDist); } } var separationWeight = 15; moveX += separationX * separationWeight; moveY += separationY * separationWeight; var moveMagnitude = Math.sqrt(moveX * moveX + moveY * moveY); if (moveMagnitude > 0) { var finalMoveX = moveX / moveMagnitude * self.speed; var finalMoveY = moveY / moveMagnitude * self.speed; self.x += finalMoveX; self.y += finalMoveY; } if (self.y < grid.y) { self.die(); } }; 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 Golem = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; self.maxHealth = 130; self.health = self.maxHealth; self.damage = 2; self.attackSpeed = 100; self.range = 1.2 * CELL_SIZE; self.lastAttack = 0; self.targetEnemy = null; self.lifetime = 1200; self.isFadingOut = false; var golemGraphics = self.attachAsset('unit_golem', { anchorX: 0.5, anchorY: 0.5 }); 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 = -golemGraphics.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.takeDamage = function (amount) { self.health -= amount; if (self.health < 0) { self.health = 0; } self.healthBar.width = self.health / self.maxHealth * 70; }; self.findClosestEnemy = function () { var closestEnemy = null; var minDistanceSq = self.range * self.range; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.isFlying) { continue; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistanceSq) { minDistanceSq = distanceSq; closestEnemy = enemy; } } return closestEnemy; }; self.attack = function () { if (!self.targetEnemy || self.targetEnemy.health <= 0) { return; } self.targetEnemy.takeDamage(self.damage); createHitSparks(self.targetEnemy.x, self.targetEnemy.y); }; self.die = function () { self.isFadingOut = true; tween(self, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { var unitIndex = friendlyUnits.indexOf(self); if (unitIndex !== -1) { friendlyUnits.splice(unitIndex, 1); } self.destroy(); } }); }; self.update = function () { if (self.isFadingOut) { return; } self.lifetime--; if (self.lifetime <= 0 || self.health <= 0) { self.die(); return; } if (!self.targetEnemy || self.targetEnemy.health <= 0 || !self.targetEnemy.parent) { self.targetEnemy = self.findClosestEnemy(); } if (self.targetEnemy) { 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.range) { if (LK.ticks - self.lastAttack >= self.attackSpeed) { self.attack(); self.lastAttack = LK.ticks; } } } }; return self; }); var GolemSummonEffect = Container.expand(function (x, y, onComplete) { var self = Container.call(this); self.x = x; self.y = y; var frameNames = []; for (var i = 1; i <= 11; i++) { frameNames.push('golem_summon_' + i); } var allFrames = []; var animationSize = 250; for (var i = 0; i < frameNames.length; i++) { var frameSprite = self.attachAsset(frameNames[i], { anchorX: 0.5, anchorY: 0.5, width: animationSize, height: animationSize }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 6; var frameTicker = 0; if (allFrames.length > 0) { allFrames[currentFrame].visible = true; } self.update = function () { frameTicker++; if (frameTicker >= frameDuration) { frameTicker = 0; if (allFrames[currentFrame]) { allFrames[currentFrame].visible = false; } currentFrame++; if (currentFrame >= allFrames.length) { if (onComplete) { onComplete(); } self.destroy(); } else { if (allFrames[currentFrame]) { allFrames[currentFrame].visible = true; } } } }; return self; }); var Grid = Container.expand(function (gridWidth, gridHeight) { var self = Container.call(this); self.cells = []; for (var i = 0; i < gridWidth; i++) { self.cells[i] = []; for (var j = 0; j < gridHeight; j++) { self.cells[i][j] = { targets: {}, score: {}, aggressionTargets: {}, aggressionScore: {}, towersInRange: [], specialEffect: null }; } } for (var i = 0; i < gridWidth; i++) { for (var j = 0; j < gridHeight; j++) { var cell = self.cells[i][j]; cell.x = i; cell.y = j; cell.up = self.cells[i - 1] && self.cells[i - 1][j]; cell.left = self.cells[i][j - 1]; cell.right = self.cells[i][j + 1]; cell.down = self.cells[i + 1] && self.cells[i + 1][j]; } } self.getCell = function (x, y) { return self.cells[x] && self.cells[x][y]; }; self.calculatePath = function (pathId, goalCell) { var toProcess = [goalCell]; var pathCounter = Math.random(); for (var a = 0; a < toProcess.length; a++) { toProcess[a].pathId = pathCounter; toProcess[a].score[pathId] = 0; } var maxScore = 0; function processNode(node, targetValue, targetNode) { if (node && (node.type === 0 || node.type === 4)) { if (!node.score[pathId] || targetValue < node.score[pathId]) { node.targets[pathId] = [targetNode]; node.score[pathId] = targetValue; if (node.pathId != pathCounter) { toProcess.push(node); node.pathId = pathCounter; } if (targetValue > maxScore) { maxScore = targetValue; } } else if (targetValue === node.score[pathId]) { node.targets[pathId].push(targetNode); } } } while (toProcess.length) { var nodes = toProcess; toProcess = []; for (var a = 0; a < nodes.length; a++) { var node = nodes[a]; var targetScore = node.score[pathId] + 10000; processNode(node.up, targetScore, node); processNode(node.right, targetScore, node); processNode(node.down, targetScore, node); processNode(node.left, targetScore, node); } } }; self.renderDebug = function () {}; self.updateEnemy = function (enemy) { var cell = grid.getCell(enemy.cellX, enemy.cellY); if (cell && cell.type === 4) { return true; } if (enemy.isFlying) { // Logika dla latajacych zostaje uproszczona, mozna rozbudowac return false; } if (!enemy.currentTarget) { if (cell && cell.targets[enemy.spawnerId]) { var targets = cell.targets[enemy.spawnerId]; enemy.currentTarget = targets[Math.floor(Math.random() * targets.length)]; } } if (enemy.currentTarget) { var laneData = levelData.spawnPoints.find(function (sp) { return sp.id === enemy.spawnerId; }); var laneOffset = (enemy.laneIndex - (laneData.lanes - 1) / 2) * laneData.laneOffset; var targetX = enemy.currentTarget.x + laneOffset; var targetY = enemy.currentTarget.y; var ox = targetX - enemy.currentCellX; var oy = targetY - enemy.currentCellY; var dist = Math.sqrt(ox * ox + oy * oy); if (dist < enemy.speed) { enemy.currentCellX = targetX; enemy.currentCellY = targetY; enemy.cellX = Math.round(enemy.currentTarget.x); enemy.cellY = Math.round(enemy.currentTarget.y); enemy.currentTarget = undefined; } else { 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; return false; }; }); var HealingFieldEffect = Container.expand(function (x, y, spellData) { var self = Container.call(this); self.x = x; self.y = y; var radius = spellData.radius; var radiusSq = radius * radius; var duration = 360; var healAmount = spellData.heal; var healInterval = 60; var particleInterval = 15; var healTicker = 0; var particleTicker = 0; var isFadingOut = false; var magicCircle = self.attachAsset('magic_circle_heal', { anchorX: 0.5, anchorY: 0.5, width: radius * 2, height: radius * 2 }); function pulse() { if (!self.parent || isFadingOut) { return; } tween(magicCircle, { alpha: 0.4 }, { duration: 1500, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.parent || isFadingOut) { return; } tween(magicCircle, { alpha: 0.8 }, { duration: 1500, easing: tween.easeInOut, onFinish: pulse }); } }); } magicCircle.alpha = 0; tween(magicCircle, { alpha: 0.8 }, { duration: 500, onFinish: pulse }); self.update = function () { if (isFadingOut) { return; } duration--; if (duration <= 0) { isFadingOut = true; tween(self, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { self.destroy(); } }); return; } healTicker++; if (healTicker >= healInterval) { healTicker = 0; friendlyUnits.forEach(function (unit) { var dx = unit.x - self.x; var dy = unit.y - self.y; if (dx * dx + dy * dy < radiusSq) { unit.health = Math.min(unit.maxHealth, unit.health + healAmount); unit.healthBar.width = unit.health / unit.maxHealth * 70; } }); } particleTicker++; if (particleTicker >= particleInterval) { particleTicker = 0; var angle = Math.random() * 2 * Math.PI; var dist = Math.sqrt(Math.random()) * radius; var pX = Math.cos(angle) * dist; var pY = Math.sin(angle) * dist; var particle = new HealingParticle(pX, pY); self.addChild(particle); } }; return self; }); var HealingParticle = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; var particleGraphics = self.attachAsset('particle_heal_plus', { anchorX: 0.5, anchorY: 0.5 }); particleGraphics.tint = 0xFFC0CB; self.alpha = 0; tween(self, { alpha: 0.8, y: self.y - 20 }, { duration: 1500, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 0, y: self.y - 30 }, { duration: 1500, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); return self; }); var LightningStrike = Container.expand(function (x, y, type) { var self = Container.call(this); self.x = x; self.y = y; var frameNames = []; for (var i = 1; i <= 5; i++) { frameNames.push('lightning_' + type + '_' + i); } var allFrames = []; for (var i = 0; i < frameNames.length; i++) { var frameSprite = self.attachAsset(frameNames[i], { anchorX: 0.5, anchorY: 0.8 }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 5; var frameTicker = 0; if (allFrames.length > 0) { allFrames[currentFrame].visible = true; } self.update = function () { if (allFrames.length === 0) { self.destroy(); return; } frameTicker++; if (frameTicker >= frameDuration) { frameTicker = 0; if (allFrames[currentFrame]) { allFrames[currentFrame].visible = false; } currentFrame++; if (currentFrame >= allFrames.length) { self.destroy(); } else { if (allFrames[currentFrame]) { allFrames[currentFrame].visible = true; } } } }; return self; }); var LightningStrikeEffect = Container.expand(function (x, y) { var self = Container.call(this); self.x = x; self.y = y; var frameNames = []; for (var i = 1; i <= 7; i++) { frameNames.push('lightning_strike_' + i); } var allFrames = []; for (var i = 0; i < frameNames.length; i++) { var frameSprite = self.attachAsset(frameNames[i], { anchorX: 0.5, anchorY: 0.9 }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 3; var frameTicker = 0; if (allFrames.length > 0) { allFrames[currentFrame].visible = true; } self.update = function () { if (allFrames.length === 0) { self.destroy(); return; } frameTicker++; if (frameTicker >= frameDuration) { frameTicker = 0; if (allFrames[currentFrame]) { allFrames[currentFrame].visible = false; } currentFrame++; if (currentFrame >= allFrames.length) { self.destroy(); } else { if (allFrames[currentFrame]) { allFrames[currentFrame].visible = true; } } } }; 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 PlacementPreview = Container.expand(function () { var self = Container.call(this); self.itemType = 'tower'; self.subType = 'guard'; self.itemSize = { w: 2, h: 2 }; self.hasEnoughGold = true; self.canPlace = false; self.gridX = 0; self.gridY = 0; var rangeIndicator = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.3 }); var previewHolder = new Container(); self.addChild(previewHolder); self.updateAppearance = function () { while (previewHolder.children.length) { previewHolder.removeChildAt(0); } var previewGraphics; // Używamy niezawodnego kształtu 'cell' jako bazy dla podglądu previewGraphics = previewHolder.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); // Ustawiamy prawidłowy rozmiar i przezroczystość previewGraphics.width = CELL_SIZE * self.itemSize.w; previewGraphics.height = CELL_SIZE * self.itemSize.h; previewGraphics.alpha = 0.5; // Domyślny, niebieski kolor dla prawidłowego miejsca previewGraphics.tint = 0x00BFFF; rangeIndicator.visible = false; if (self.itemType === 'tower') { var stats = TOWER_DATA[self.subType] ? TOWER_DATA[self.subType][1] : null; if (stats && stats.range) { rangeIndicator.width = rangeIndicator.height = stats.range * 2; rangeIndicator.visible = true; } } // Jeśli miejsce jest nieprawidłowe, zmieniamy kolor na czerwony if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; } }; self.updatePlacementStatus = function (targetGrid) { var validGridPlacement = true; var currentGrid = targetGrid || grid; if (self.gridY <= 4 && self.itemType === 'tower') { validGridPlacement = false; } if (self.gridY + self.itemSize.h > currentGrid.cells[0].length && self.itemType === 'building') { validGridPlacement = false; } if (validGridPlacement) { for (var i = 0; i < self.itemSize.w; i++) { for (var j = 0; j < self.itemSize.h; j++) { var cell = currentGrid.getCell(self.gridX + i, self.gridY + j); if (!cell || cell.type !== 1) { validGridPlacement = false; break; } } if (!validGridPlacement) { break; } } } var cost = 0; if (self.itemType === 'tower') { cost = getTowerCost(self.subType); } else { var buildingCost = getBuildingCost(self.subType); cost = buildingCost.gold; } self.hasEnoughGold = gold >= cost; self.canPlace = validGridPlacement; self.updateAppearance(); }; self.snapToGrid = function (x, y, targetGrid) { var currentGrid = targetGrid || grid; var gridPosX = x - currentGrid.x; var gridPosY = y - currentGrid.y; self.gridX = Math.floor(gridPosX / CELL_SIZE); self.gridY = Math.floor(gridPosY / CELL_SIZE); var itemWidth = CELL_SIZE * self.itemSize.w; var itemHeight = CELL_SIZE * self.itemSize.h; self.x = currentGrid.x + self.gridX * CELL_SIZE + itemWidth / 2; self.y = currentGrid.y + self.gridY * CELL_SIZE + itemHeight / 2; self.updatePlacementStatus(currentGrid); }; return self; }); var SourceBuilding = Container.expand(function (buildingType) { var self = Container.call(this); self.buildingType = buildingType || 'default'; var buildingData = BUILDING_DATA[self.buildingType]; var assetId = 'building_' + self.buildingType; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.width = 130; baseGraphics.height = 130; var typeLabelShadow = new Text2(buildingData.name, { size: 50, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 4; typeLabelShadow.y = -20 + 4; self.addChild(typeLabelShadow); var typeLabel = new Text2(buildingData.name, { size: 50, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; self.addChild(typeLabel); var costLabelShadow = new Text2(buildingData.cost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); var costLabel = new Text2(buildingData.cost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { var currentCost = getBuildingCost(self.buildingType); if (costLabel.text !== currentCost.gold) { costLabel.setText(currentCost.gold); costLabelShadow.setText(currentCost.gold); } var canAfford = gold >= currentCost.gold; self.alpha = canAfford ? 1 : 0.5; }; return self; }); var SourceSpell = Container.expand(function (spellId) { var self = Container.call(this); self.spellId = spellId; var spellData = SPELL_DATA[self.spellId]; var baseGraphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); switch (self.spellId) { case 'fire_wall': baseGraphics.tint = 0xFF4500; break; case 'healing_field': baseGraphics.tint = 0x32CD32; break; case 'lightning_stun': baseGraphics.tint = 0x1E90FF; break; case 'curse': baseGraphics.tint = 0x9400D3; break; } var nameLabelShadow = new Text2(spellData.name, { size: 40, fill: 0x000000, weight: 800, align: 'center' }); nameLabelShadow.anchor.set(0.5, 0.5); nameLabelShadow.x = 2; nameLabelShadow.y = -20 + 2; self.addChild(nameLabelShadow); var nameLabel = new Text2(spellData.name, { size: 40, fill: 0xFFFFFF, weight: 800, align: 'center' }); nameLabel.anchor.set(0.5, 0.5); nameLabel.y = -20; self.addChild(nameLabel); var costLabelShadow = new Text2(spellData.cost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); var costLabel = new Text2(spellData.cost, { size: 50, fill: 0x00BFFF, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { var canAfford = mana >= spellData.cost; self.alpha = canAfford ? 1 : 0.5; }; return self; }); var SourceTower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType || 'default'; var assetId = 'tower_' + self.towerType; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.isTowerBase = true; baseGraphics.width = 130; baseGraphics.height = 170; var stats = TOWER_DATA[self.towerType][1]; // Pobierz dane dla poziomu 1 var towerName = stats.name; var initialCost = stats.cost; var typeLabelShadow = new Text2(towerName, { size: 40, fill: 0x000000, weight: 800 }); typeLabelShadow.anchor.set(0.5, 0.5); typeLabelShadow.x = 2; typeLabelShadow.y = -20 + 2; self.addChild(typeLabelShadow); var typeLabel = new Text2(towerName, { size: 40, fill: 0xFFFFFF, weight: 800 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; self.addChild(typeLabel); var costLabelShadow = new Text2(initialCost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); var costLabel = new Text2(initialCost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { // Ta funkcja będzie aktualizować koszt w przyszłości, gdy wprowadzimy dynamiczne zmiany var currentCost = TOWER_DATA[self.towerType][1].cost; if (!isBuildPhase && currentWave > 0) { currentCost = Math.floor(currentCost * 1.5); } if (costLabel.text !== currentCost) { costLabel.setText(currentCost); costLabelShadow.setText(currentCost); } var canAfford = gold >= currentCost; self.alpha = canAfford ? 1 : 0.5; }; return self; }); var SourceUnit = Container.expand(function (unitId) { var self = Container.call(this); self.unitId = unitId; self.interactive = true; var unitData = UNIT_DATA[self.unitId]; var assetId = 'unit_' + unitId; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.width = 160; baseGraphics.height = 180; var nameLabelShadow = new Text2(unitData.name, { size: 40, fill: 0x000000, weight: 800, align: 'center' }); nameLabelShadow.anchor.set(0.5, 0.5); nameLabelShadow.x = 2; nameLabelShadow.y = -20 + 2; self.addChild(nameLabelShadow); var nameLabel = new Text2(unitData.name, { size: 40, fill: 0xFFFFFF, weight: 800, align: 'center' }); nameLabel.anchor.set(0.5, 0.5); nameLabel.y = -20; self.addChild(nameLabel); var costLabelShadow = new Text2(unitData.cost, { size: 50, fill: 0x000000, weight: 800 }); costLabelShadow.anchor.set(0.5, 0.5); costLabelShadow.x = 4; costLabelShadow.y = 24 + 12; self.addChild(costLabelShadow); var costLabel = new Text2(unitData.cost, { size: 50, fill: 0xFFD700, weight: 800 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { var canAfford = gold >= unitData.cost && people > 0; self.alpha = canAfford ? 1 : 0.5; }; self.down = function () { if (LK.ticks - lastRecruitActionTick < 10) { return; } if (gold >= unitData.cost && people > 0) { lastRecruitActionTick = LK.ticks; var hasSmithy = castleBuildings.some(function (b) { return b.id === 'smithy'; }); var finalStats = Object.assign({}, unitData); if (hasSmithy) { finalStats.maxHealth = Math.floor(finalStats.maxHealth * 1.15); finalStats.damage = Math.floor(finalStats.damage * 1.15); } setGold(gold - finalStats.cost); people--; updateUI(); panelActionTaken = true; var spawnLaneOffset = 80; var spawnLanes = [2048 / 2 - spawnLaneOffset, 2048 / 2, 2048 / 2 + spawnLaneOffset]; var spawnX = spawnLanes[Math.floor(Math.random() * spawnLanes.length)]; spawnX += Math.random() * 40 - 20; var spawnY = grid.y + (grid.cells[0].length - 3) * CELL_SIZE; var newUnit = new FriendlyUnit(spawnX, spawnY, finalStats); friendlyUnitLayer.addChild(newUnit); friendlyUnits.push(newUnit); } else { var message = gold < unitData.cost ? "Not enough gold!" : "Not enough people!"; var notification = game.addChild(new Notification(message)); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; return self; }); var SpellPreview = Container.expand(function () { var self = Container.call(this); self.visible = false; self.spellId = null; self.rangeIndicator = self.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.4 }); self.rangeIndicator.blendMode = 1; self.unitIndicator = self.attachAsset('unit_golem', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5, width: 160, height: 160, visible: false }); self.getActiveGraphic = function () { if (self.spellId === 'summon_golem') { return self.unitIndicator; } return self.rangeIndicator; }; self.updateAppearance = function (spellData, canAfford) { self.spellId = spellData.id; if (spellData.id === 'summon_golem') { self.rangeIndicator.visible = false; self.unitIndicator.visible = true; } else { self.rangeIndicator.visible = true; self.unitIndicator.visible = false; self.rangeIndicator.width = self.rangeIndicator.height = spellData.radius * 2; } var activeGraphic = self.getActiveGraphic(); if (activeGraphic) { activeGraphic.tint = canAfford ? 0x00BFFF : 0xFF0000; } self.visible = true; }; self.updatePosition = function (x, y) { self.x = x; self.y = y; }; return self; }); var ThunderstormEffect = Container.expand(function (x, y, spellData) { var self = Container.call(this); self.x = x; self.y = y; var radius = spellData.radius; var radiusSq = radius * radius; var totalStrikes = 12; var stormDuration = 90; var strikesSpawned = 0; var spawnTicker = 0; var timeBetweenStrikes = stormDuration / totalStrikes; enemies.forEach(function (enemy) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; if (dx * dx + dy * dy < radiusSq) { enemy.isStunned = true; enemy.stunTimer = spellData.stunDuration; } }); self.update = function () { spawnTicker++; if (spawnTicker >= timeBetweenStrikes) { spawnTicker = 0; strikesSpawned++; if (strikesSpawned > totalStrikes) { self.destroy(); return; } var angle = Math.random() * 2 * Math.PI; var dist = Math.sqrt(Math.random()) * radius; var strikeX = self.x + Math.cos(angle) * dist; var strikeY = self.y + Math.sin(angle) * dist; var type = Math.random() > 0.5 ? 'white' : 'yellow'; var strike = new LightningStrike(strikeX, strikeY, type); spellEffectLayer.addChild(strike); } }; return self; }); var Tower = Container.expand(function (towerType) { var self = Container.call(this); self.towerType = towerType; self.level = 1; self.levelPath = ''; self.maxLevel = 3; self.gridX = 0; self.gridY = 0; self.targetEnemy = null; self.lastFired = 0; self.totalValue = 0; self.isConstructing = false; self.constructionContainer = null; self.timerText = null; self.auraEffectContainer = null; self.baseStats = {}; self.buffs = []; self.towersInAura = []; if (self.towerType === 'banner') { self.auraCheckTimer = 0; self.particleSpawnTimer = 0; self.particleSpawnRate = 190; } function setTowerSize(graphic) { var towerScaleX = 2.4; var towerScaleY = 3.9; graphic.width = CELL_SIZE * towerScaleX; graphic.height = CELL_SIZE * towerScaleY; } function applyStats(stats) { self.baseStats = Object.assign({}, stats); Object.assign(self, stats); self.recalculateStats(); } self.recalculateStats = function () { self.damage = self.baseStats.damage; self.fireRate = self.baseStats.fireRate; self.buffs.forEach(function (buff) { if (buff.effect.damage) { self.damage *= buff.effect.damage; } if (buff.effect.attackSpeed) { self.fireRate /= buff.effect.attackSpeed; } }); }; self.addBuff = function (buff) { if (!self.buffs.find(function (b) { return b.source === buff.source; })) { self.buffs.push(buff); self.recalculateStats(); } }; self.removeBuff = function (buffSource) { self.buffs = self.buffs.filter(function (b) { return b.source !== buffSource; }); self.recalculateStats(); }; var initialStats = TOWER_DATA[self.towerType][self.level]; self.cost = initialStats.cost; self.totalValue = self.cost; applyStats(initialStats); var assetId = 'tower_' + self.towerType; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.isTowerBase = true; setTowerSize(baseGraphics); self.getRange = function () { return self.range; }; self.getTotalValue = function () { return self.totalValue; }; self.upgrade = function (path) { if (self.isConstructing) { return; } path = path || ''; var nextLevelKey = self.level + 1 + path; var upgradeData = TOWER_DATA[self.towerType][nextLevelKey]; if (!upgradeData) { return; } self.startConstructionAnimation(5, function () { if (self.aura) { self.removeAuraFromTowers(); } self.level++; self.levelPath = path; self.totalValue += upgradeData.cost; var stats = TOWER_DATA[self.towerType][self.level + self.levelPath]; applyStats(stats); var oldGraphics = self.children[0]; if (oldGraphics.isTowerBase) { oldGraphics.destroy(); } var newAssetId = 'tower_' + self.towerType + '_' + self.level + self.levelPath; var newGraphics = self.attachAsset(newAssetId, { anchorX: 0.5, anchorY: 0.5 }); newGraphics.isTowerBase = true; setTowerSize(newGraphics); self.addChildAt(newGraphics, 0); if (self.towerType === 'crossbow' && self.gridX < 12) { newGraphics.scale.x = -1; } if (self.aura) { self.applyAuraToTowers(); } }); }; self.destroyAura = function () { if (self.aura) { self.removeAuraFromTowers(); } }; self.placeOnGrid = function (gridX, gridY) { self.gridX = gridX; self.gridY = gridY; self.x = grid.x + (gridX + 1) * CELL_SIZE; self.y = grid.y + (gridY + 1) * CELL_SIZE; if (self.towerType === 'crossbow' && self.gridX < 12) { var towerGraphic = self.children[0]; if (towerGraphic) { towerGraphic.scale.x = -1; } } for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 5; } } } if (self.aura) { self.applyAuraToTowers(); } }; self.findTarget = function () { if (self.damage === 0) { return null; } if (self.bulletType === 'slow') { var potentialTargets = []; enemies.forEach(function (enemy) { if (!enemy.slowed) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; if (dx * dx + dy * dy < self.range * self.range) { potentialTargets.push(enemy); } } }); if (potentialTargets.length === 0) { enemies.forEach(function (enemy) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; if (dx * dx + dy * dy < self.range * self.range) { potentialTargets.push(enemy); } }); } if (potentialTargets.length > 0) { return potentialTargets[Math.floor(Math.random() * potentialTargets.length)]; } } var closestEnemy = null; var minDistance = self.range * self.range; 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 distanceSq = dx * dx + dy * dy; if (distanceSq < minDistance) { minDistance = distanceSq; closestEnemy = enemy; } } return closestEnemy; }; self.fire = function () { if (self.targetEnemy && self.targetEnemy.health > 0) { var dx = self.targetEnemy.x - self.x; var dy = self.targetEnemy.y - self.y; var angle = Math.atan2(dy, dx); var bulletX = self.x + Math.cos(angle) * 40; var bulletY = self.y + Math.sin(angle) * 40; var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self); if (self.towerType === 'mage') { spellEffectLayer.addChild(bullet); } else { gateContainer.addChild(bullet); } bullets.push(bullet); } }; self.applyAuraToTowers = function () { self.removeAuraFromTowers(); towers.forEach(function (tower) { if (tower === self) { return; } var dx = tower.x - self.x; var dy = tower.y - self.y; if (dx * dx + dy * dy < self.range * self.range) { tower.addBuff({ source: self, effect: self.aura }); self.towersInAura.push(tower); } }); }; self.removeAuraFromTowers = function () { self.towersInAura.forEach(function (tower) { tower.removeBuff(self); }); self.towersInAura = []; }; self.startConstructionAnimation = function (duration, onComplete) { self.isConstructing = true; var baseGraphics = self.children[0]; if (baseGraphics) { baseGraphics.visible = false; } if (self.constructionContainer) { self.constructionContainer.destroy(); } self.constructionContainer = new Container(); self.addChild(self.constructionContainer); var constructionAssets = []; for (var i = 1; i <= 5; i++) { var assetName = 'tower_construction_' + i; var asset = self.constructionContainer.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); asset.alpha = i === 1 ? 1 : 0; constructionAssets.push(asset); } if (self.timerText) { self.timerText.destroy(); } self.timerText = new Text2(duration.toString(), { size: 80, fill: 0xFFFFFF, weight: 800, stroke: 0x000000, strokeThickness: 10 }); self.timerText.anchor.set(0.5, 0.5); self.timerText.y = -CELL_SIZE * 1.5; self.addChild(self.timerText); function animateStep(stage) { if (stage > duration) { if (baseGraphics) { baseGraphics.visible = true; } self.constructionContainer.destroy(); self.constructionContainer = null; self.timerText.destroy(); self.timerText = null; self.isConstructing = false; if (onComplete) { onComplete(); } return; } self.timerText.setText((duration - stage + 1).toString()); var currentAsset = constructionAssets[stage - 1]; var prevAsset = constructionAssets[stage - 2]; if (prevAsset) { tween(prevAsset, { alpha: 0 }, { duration: 200 }); } if (currentAsset) { tween(currentAsset, { alpha: 1 }, { duration: 300 }); } LK.setTimeout(function () { animateStep(stage + 1); }, 1000); } animateStep(1); }; self.update = function () { if (self.isConstructing) { return; } if (self.towerType === 'banner') { self.auraCheckTimer++; if (self.auraCheckTimer > 60) { self.auraCheckTimer = 0; self.applyAuraToTowers(); } self.particleSpawnTimer++; if (self.particleSpawnTimer >= self.particleSpawnRate) { self.particleSpawnTimer = 0; self.towersInAura.forEach(function (buffedTower) { if (buffedTower && buffedTower.parent) { var particle = new Container(); particle.attachAsset('aura_particle', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); var spread = 80; particle.x = buffedTower.x + (Math.random() * spread - spread / 2); particle.y = buffedTower.y + 100; gateContainer.addChild(particle); tween(particle, { y: buffedTower.y - 120, alpha: 0 }, { duration: 4000 + Math.random() * 1000, easing: tween.easeIn, onFinish: function onFinish() { particle.destroy(); } }); } }); } return; } self.targetEnemy = self.findTarget(); if (self.targetEnemy) { if (self.fireRate > 0 && LK.ticks - self.lastFired >= self.fireRate) { self.fire(); self.lastFired = LK.ticks; } } }; self.down = function (x, y, obj) { if (isMenuTransitioning || self.isConstructing) { return; } if (selectedTower === self) { closeActiveUpgradeMenu(); return; } closeActiveUpgradeMenu(); selectedTower = self; var upgradeMenu = game.addChild(new UpgradeMenu(self)); upgradeMenu.x = 2048 / 2; isMenuTransitioning = true; tween(upgradeMenu, { y: 2732 - 250 }, { duration: 200, easing: tween.backOut, onFinish: function onFinish() { isMenuTransitioning = false; } }); var rangeCircle = new Container(); rangeCircle.isTowerRange = true; rangeCircle.tower = self; var graphics = rangeCircle.attachAsset('rangeCircle', { anchorX: 0.5, anchorY: 0.5 }); graphics.width = graphics.height = self.getRange() * 2; graphics.alpha = 0.2; graphics.tint = 0xFFFFFF; rangeCircle.x = self.x; rangeCircle.y = self.y; gateContainer.addChildAt(rangeCircle, gateContainer.children.indexOf(towerLayer)); }; 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.name, { size: 80, fill: 0xFFFFFF, weight: 800 }); towerTypeText.anchor.set(0, 0); towerTypeText.x = -840; towerTypeText.y = -160; self.addChild(towerTypeText); function getTowerFireRate() { if (self.tower.fireRate > 0) { return (60 / self.tower.fireRate).toFixed(1) + '/s'; } return 'N/A'; } var statsText = new Text2('', { size: 70, fill: 0xFFFFFF, weight: 400 }); statsText.anchor.set(0, 0.5); statsText.x = -840; statsText.y = 50; self.addChild(statsText); var sellButton = new Container(); var sellButtonBackground = sellButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 400, height: 120, tint: 0xCC0000 }); var sellValue = getTowerSellValue(self.tower.getTotalValue()); var sellButtonText = new Text2('Sell: +' + sellValue, { size: 50, fill: 0xFFFFFF, weight: 800 }); sellButtonText.anchor.set(0.5, 0.5); sellButton.addChild(sellButtonText); sellButton.x = 650; sellButton.y = 120; self.addChild(sellButton); sellButton.down = function () { if (self.tower.aura) { self.tower.destroyAura(); } setGold(gold + getTowerSellValue(self.tower.getTotalValue())); var gridX = self.tower.gridX; var gridY = self.tower.gridY; for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cell = grid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 1; } } } // --- POCZĄTEK POPRAWKI --- // Przeliczamy na nowo wszystkie zdefiniowane ścieżki if (levelData.spawnPoints) { levelData.spawnPoints.forEach(function (spawner) { var goalCell = grid.getCell(spawner.goalX, spawner.goalY); if (goalCell) { grid.calculatePath(spawner.id, goalCell); } }); } // --- KONIEC POPRAWKI --- if (selectedTower === self.tower) { selectedTower = null; } var towerIndex = towers.indexOf(self.tower); if (towerIndex !== -1) { towers.splice(towerIndex, 1); } towerLayer.removeChild(self.tower); hideUpgradeMenu(self); }; var closeButton = new Container(); var closeBackground = closeButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 90, height: 90, 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; self.addChild(closeButton); closeButton.down = function () { hideUpgradeMenu(self); }; function createUpgradeButton(upgradeKey, position, totalButtons) { var upgradeData = TOWER_DATA[self.tower.towerType][upgradeKey]; if (!upgradeData) { return; } var button = new Container(); var bg = button.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5 }); bg.width = 450; bg.height = 180 / totalButtons + 60 / totalButtons; var buttonTextContent = 'Upgrade: ' + upgradeData.cost + 'g'; if (upgradeData.name) { buttonTextContent = upgradeData.name + '\n' + upgradeData.cost + 'g'; if (upgradeData.description) { buttonTextContent += '\n' + upgradeData.description; } } var text = new Text2(buttonTextContent, { size: 40, fill: 0xFFFFFF, weight: 800, align: 'center' }); text.anchor.set(0.5, 0.5); button.addChild(text); var canAfford = gold >= upgradeData.cost; bg.tint = canAfford ? 0x00AA00 : 0x888888; button.alpha = canAfford ? 1 : 0.7; button.down = function () { if (gold >= upgradeData.cost) { setGold(gold - upgradeData.cost); self.tower.upgrade(upgradeKey.replace((self.tower.level + 1).toString(), '')); hideUpgradeMenu(self); } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } }; button.x = position.x; button.y = position.y; self.addChild(button); } function populateUpgradeButtons() { var hasWorkshop = castleBuildings.some(function (b) { return b.id === 'workshop'; }); if (self.tower.level >= (hasWorkshop ? self.tower.maxLevel : 2)) { var maxLevelText = new Text2('Max Level', { size: 80, fill: 0xFFD700, weight: 800 }); if (self.tower.level === 2 && !hasWorkshop) { maxLevelText.setText("Requires Workshop"); maxLevelText.size = 50; } maxLevelText.anchor.set(0.5, 0.5); maxLevelText.x = 50; self.addChild(maxLevelText); return; } var nextLevel = self.tower.level + 1; var upgradeKeys = Object.keys(TOWER_DATA[self.tower.towerType]).filter(function (key) { if (!key.startsWith(nextLevel.toString())) { return false; } if (nextLevel >= 3 && !hasWorkshop) { return false; } return true; }); if (upgradeKeys.length === 1) { createUpgradeButton(upgradeKeys[0], { x: 50, y: 0 }, 1); } else if (upgradeKeys.length > 1) { for (var i = 0; i < upgradeKeys.length; i++) { var yPos = -85 + i * 170; createUpgradeButton(upgradeKeys[i], { x: 50, y: yPos }, upgradeKeys.length); } } } populateUpgradeButtons(); self.update = function () { var currentFireRateDisplay = getTowerFireRate(); var newStatsString = 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage.toFixed(1) + '\nFire Rate: ' + currentFireRateDisplay; if (statsText.text !== newStatsString) { statsText.setText(newStatsString); } }; var originalDestroy = self.destroy; self.destroy = function () { for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isTowerRange && game.children[i].tower === self.tower) { game.removeChild(game.children[i]); } } if (selectedTower === self.tower) { selectedTower = null; } originalDestroy.call(self); }; return self; }); /**** * Initialize Game ****/ /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ /**** * Game Code ****/ function hideBuildingInfoMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; isMenuTransitioning = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; isMenuTransitioning = false; } }); } function createHitSparks(x, y) { var numSparks = 5; for (var i = 0; i < numSparks; i++) { var spark = new Container(); var sparkGraphics = spark.attachAsset('star_particle', { anchorX: 0.5, anchorY: 0.5, tint: 0xFFD700 }); spark.x = x; spark.y = y; gateContainer.addChild(spark); var angle = Math.random() * Math.PI * 2; var distance = Math.random() * 40 + 20; tween(spark, { x: x + Math.cos(angle) * distance, y: y + Math.sin(angle) * distance, alpha: 0 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { spark.destroy(); } }); } } function showSpellOverlay() { spellOverlay.visible = true; tween(spellOverlay, { alpha: 0.7 }, { duration: 650, easing: tween.easeOut }); } function hideSpellOverlay() { tween(spellOverlay, { alpha: 0 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { spellOverlay.visible = false; } }); } var CELL_SIZE = 76; var TOWER_DATA = { 'guard': { 1: { name: 'Guard Tower', cost: 30, damage: 5, fireRate: 60, range: 5 * CELL_SIZE, bulletSpeed: 7, graphic: 'arrow_guard' }, 2: { name: 'Veteran Guard', cost: 80, // Zwiększono z 60 damage: 9, fireRate: 60, range: 5 * CELL_SIZE, bulletSpeed: 7, graphic: 'arrow_guard' }, 3: { name: 'Elite Guard', cost: 155, // Zwiększono ze 120 damage: 17, fireRate: 55, range: 5.5 * CELL_SIZE, bulletSpeed: 7, graphic: 'arrow_guard' } }, 'crossbow': { 1: { name: 'Crossbow Tower', cost: 50, damage: 13, fireRate: 150, range: 7 * CELL_SIZE, bulletSpeed: 15, graphic: 'arrow_crossbow' }, 2: { name: 'Heavy Crossbow', cost: 130, // Zwiększono ze 100 damage: 25, fireRate: 150, range: 7 * CELL_SIZE, bulletSpeed: 15, graphic: 'arrow_crossbow' }, 3: { name: 'Arbalest', cost: 260, // Zwiększono z 200 damage: 51, fireRate: 150, range: 7.5 * CELL_SIZE, bulletSpeed: 15, graphic: 'arrow_crossbow' } }, 'mage': { 1: { name: 'Mage Tower', cost: 40, damage: 7, fireRate: 90, range: 5 * CELL_SIZE, bulletSpeed: 8, bulletType: 'normal', graphic: 'bullet' }, '2F': { name: 'Fire Mage', cost: 105, // Zwiększono z 80 damage: 7, fireRate: 120, range: 6 * CELL_SIZE, bulletSpeed: 8, bulletType: 'splash', splashDamage: 3, splashRadius: 1 * CELL_SIZE, graphic: 'projectile_fireball' }, '3F': { name: 'Pyromancer', cost: 210, // Zwiększono ze 160 damage: 13, fireRate: 120, range: 6.5 * CELL_SIZE, bulletSpeed: 8, bulletType: 'splash', splashDamage: 8, splashRadius: 1.5 * CELL_SIZE, graphic: 'projectile_fireball' }, '2W': { name: 'Water Mage', cost: 105, // Zwiększono z 80 damage: 5, fireRate: 120, range: 6 * CELL_SIZE, bulletSpeed: 8, bulletType: 'slow', slowAmount: 0.6, slowDuration: 120, slowChance: 0.3, graphic: ['ice_spike_1', 'ice_spike_2', 'ice_spike_3', 'ice_spike_4', 'ice_spike_5'] }, '3W': { name: 'Ice Archmage', cost: 210, // Zwiększono ze 160 damage: 11, fireRate: 110, range: 6.5 * CELL_SIZE, bulletSpeed: 8, bulletType: 'slow', slowAmount: 0.5, slowDuration: 120, slowChance: 0.4, graphic: ['ice_spike_1', 'ice_spike_2', 'ice_spike_3', 'ice_spike_4', 'ice_spike_5'] }, '2L': { name: 'Lightning Mage', cost: 105, // Zwiększono z 80 damage: 8, fireRate: 150, range: 7 * CELL_SIZE, bulletSpeed: 20, bulletType: 'chain', chainTargets: 2, chainDamageFalloff: 2, chainChance: 0.25, graphic: 'bullet' }, '3L': { name: 'Stormcaller', cost: 210, // Zwiększono ze 160 damage: 16, fireRate: 150, range: 7.5 * CELL_SIZE, bulletSpeed: 20, bulletType: 'chain', chainTargets: 3, chainDamageFalloff: 2, chainChance: 0.35, graphic: 'bullet' } }, 'banner': { 1: { name: 'War Banner', cost: 50, damage: 0, range: 4 * CELL_SIZE, aura: { 'attackSpeed': 1.15 } }, '2A': { name: 'Banner of Fury', cost: 130, // Zwiększono ze 100 range: 4 * CELL_SIZE, aura: { 'attackSpeed': 1.3 } }, '2B': { name: 'Banner of Command', cost: 130, // Zwiększono ze 100 range: 4 * CELL_SIZE, aura: { 'attackSpeed': 1.15, 'damage': 1.15 } } } }; var BUILDING_DATA = { 'home': { id: 'home', name: 'Home', size: { w: 3, h: 3 }, cost: 50, description: 'Each Home increases your max Population by 5.' }, 'workshop': { id: 'workshop', name: 'Workshop', size: { w: 4, h: 3 }, cost: 150, description: 'Unlocks advanced tower upgrades (Level 3+).' }, 'barracks': { id: 'barracks', name: 'Barracks', size: { w: 6, h: 7 }, cost: 200, description: 'Unlocks the Army panel and allows recruitment of units.' }, 'magic_academy': { id: 'magic_academy', name: 'Magic Academy', size: { w: 6, h: 8 }, cost: 200, description: 'Unlocks Mana and the Magic panel, allowing you to cast powerful spells.' }, 'tavern': { id: 'tavern', name: 'Tavern', size: { w: 5, h: 5 }, cost: 120, description: 'Generates income each wave. Each Tavern provides 30g, plus a bonus of +2g for each person in your population.' }, 'smithy': { id: 'smithy', name: 'Smithy', size: { w: 5, h: 6 }, cost: 150, description: 'Provides global buffs to towers and units.' }, 'infirmary': { id: 'infirmary', name: 'Infirmary', size: { w: 4, h: 3 }, cost: 200, description: 'Reduces the time it takes for population to regenerate after a unit dies.' } }; var SPELL_DATA = { 'fire_wall': { id: 'fire_wall', name: 'Fire Wall', description: 'Damages enemies in an area.', cost: 0, radius: 2.5 * CELL_SIZE, duration: 460, dotDamage: 5, dotDuration: 180 }, 'healing_field': { id: 'healing_field', name: 'Healing Field', description: 'Heals allied targets in an area.', cost: 0, heal: 25, radius: 3 * CELL_SIZE }, 'lightning_stun': { id: 'lightning_stun', name: 'Thunderstorm', description: 'Stuns enemies in an area.', cost: 0, stunDuration: 2 * 60, radius: 3 * CELL_SIZE }, 'summon_golem': { id: 'summon_golem', name: 'Summon Golem', description: 'Summons a temporary blocker unit.', cost: 0, radius: 0 }, 'curse': { id: 'curse', name: 'Curse', description: 'Cursed enemies move slower and take more damage.', cost: 0, damageMultiplier: 1.10, slowFactor: 0.85, duration: 480, radius: 3.5 * CELL_SIZE } }; var LEVEL_DATA_ALL = { 1: { levelName: "Level 1: Royal Road", initialGold: 600, initialLives: 20, mapLayout: ["222222221100011222222222", "22222222AA00011222222222", "222222221100011222222222", "222222221100011222222222", "2222222211000AA222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "22222222110E011222222222"], castleMapLayout: ["11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111"], spawnPoints: [{ id: 'mainPath', startX: 12, startY: 5, goalX: 12, goalY: 34, lanes: 2, laneOffset: 1.2 }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 140, units: [{ spawner: 'mainPath', type: 'normal', count: 12 }] }] }, { wave: 2, phases: [{ delay: 0, interval: 100, units: [{ spawner: 'mainPath', type: 'normal', count: 10 }] }, { delay: 450, interval: 120, units: [{ spawner: 'mainPath', type: 'shield', count: 6 }] }] }, { wave: 3, phases: [{ delay: 0, interval: 110, units: [{ spawner: 'mainPath', type: 'normal', count: 8 }, { spawner: 'mainPath', type: 'shield', count: 3 }] }, { delay: 600, interval: 60, units: [{ spawner: 'mainPath', type: 'rider', count: 9 }] }] }, { wave: 4, phases: [{ delay: 0, interval: 130, units: [{ spawner: 'mainPath', type: 'shield', count: 10 }] }, { delay: 240, interval: 100, units: [{ spawner: 'mainPath', type: 'archer', count: 8 }] }, { delay: 500, interval: 80, units: [{ spawner: 'mainPath', type: 'rider', count: 6 }] }] }, { wave: 5, phases: [{ delay: 0, interval: 70, units: [{ spawner: 'mainPath', type: 'normal', count: 15 }] }, { delay: 400, interval: 110, units: [{ spawner: 'mainPath', type: 'shield', count: 10 }, { spawner: 'mainPath', type: 'archer', count: 8 }] }, { delay: 400, interval: 60, units: [{ spawner: 'mainPath', type: 'rider', count: 10 }] }] }, { wave: 6, phases: [{ delay: 0, interval: 100, units: [{ spawner: 'mainPath', type: 'shield', count: 10 }, { spawner: 'mainPath', type: 'archer', count: 8 }] }, { delay: 400, interval: 80, units: [{ spawner: 'mainPath', type: 'chieftain', count: 1, isBoss: true }, { spawner: 'mainPath', type: 'rider', count: 10 }] }] }] } }; var UNIT_DATA = { 'swordsman': { id: 'swordsman', name: 'Swordsman', cost: 25, speed: 1.2, maxHealth: 20, damage: 2, attackSpeed: 90, range: 1.2 * CELL_SIZE, detectionRange: 3 * CELL_SIZE, isRanged: false }, 'defender': { id: 'defender', name: 'Defender', cost: 40, speed: 0.9, maxHealth: 40, damage: 2, attackSpeed: 100, range: 1.2 * CELL_SIZE, detectionRange: 3 * CELL_SIZE, isRanged: false }, 'horseman': { id: 'horseman', name: 'Horseman', cost: 60, speed: 1.8, maxHealth: 50, damage: 5, attackSpeed: 80, range: 1.3 * CELL_SIZE, detectionRange: 3.5 * CELL_SIZE, isRanged: false }, 'archer': { id: 'archer', name: 'Archer', cost: 40, speed: 1.1, maxHealth: 15, damage: 6, attackSpeed: 48, range: 5 * CELL_SIZE, detectionRange: 6 * CELL_SIZE, isRanged: true } }; var isBuildPhase = false; var buildPhaseTimer = 0; var startGameButton; var timerText; var pathId; var buildingDeck; var spellOverlay; var spellEffectLayer; var buildingHand; var castleBuildings; var castleBuildingsLayer; var statsPanel; var statsDetailsContainer; var statsToggleButton; var isStatsPanelExpanded = false; var maxScore; var enemies; var towers; var bullets; var selectedTower; var selectedBuilding; var isDragging; var isDraggingSpell; var lastRecruitActionTick; var panelActionTaken; var gold; var lives; var mana; var numberOfTaverns; var levelData; var isManaUnlocked; var enemiesKilled; var enemiesInCurrentWave; var levelData; var currentWave; var totalWaves; var waveTimer; var waveInProgress; var waveSpawned; var nextWaveTime; var sourceTowers; var grid; var wavesText; var towerLayer; var spawnQueue; var waveClock; var enemyLayer; var debugLayer; var buildableTilesLayer; var castleBuildableTilesLayer; var specialTilesLayer; var enemyLayerBottom; var enemyLayerMiddle; var enemyLayerTop; var placementPreview; var buildPanelContainer; var buildButton; var rerollButton; var isBuildPanelOpen; var currentActiveView; var gateContainer; var castleContainer; var viewToggleButton; var castleGrid; var castleBuildPanel; var magicButton; var magicPanelContainer; var isMagicPanelOpen; var armyButton; var armyPanelContainer; var isArmyPanelOpen; var isArmyUnlocked; var panelActionTaken; var spellPreview; var activeSpells; friendlyUnits = []; var friendlyUnitLayer; var peopleRegenQueue; var currentScreenState = ''; var titleScreenContainer = null; var levelSelectContainer = null; var isHidingUpgradeMenu = false; var isMenuTransitioning = false; function hideUpgradeMenu(menu) { if (isHidingUpgradeMenu) { return; } isHidingUpgradeMenu = true; isMenuTransitioning = true; tween(menu, { y: 2732 + 225 }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { menu.destroy(); isHidingUpgradeMenu = false; isMenuTransitioning = false; } }); } function closeActiveUpgradeMenu() { if (isMenuTransitioning) { return; } game.children.forEach(function (child) { if (child instanceof UpgradeMenu) { hideUpgradeMenu(child); } }); for (var i = gateContainer.children.length - 1; i >= 0; i--) { if (gateContainer.children[i].isTowerRange) { gateContainer.removeChild(gateContainer.children[i]); } } selectedTower = null; } function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); enemiesText.setText('Enemies: ' + enemiesKilled + '/' + enemiesInCurrentWave); peopleText.setText('People: ' + people + '/' + maxPeople); wavesText.setText('Wave: ' + currentWave + '/' + totalWaves); if (isManaUnlocked) { manaText.visible = true; manaText.setText('Mana: ' + mana); } } function setGold(value) { gold = value; updateUI(); } function setMana(value) { if (isManaUnlocked) { mana = value; updateUI(); } } function getTowerCost(towerType) { var cost = 0; if (TOWER_DATA[towerType] && TOWER_DATA[towerType][1]) { cost = TOWER_DATA[towerType][1].cost; } if (!isBuildPhase && currentWave > 0) { cost = Math.floor(cost * 1.5); } return cost; } function getTowerSellValue(totalValue) { return Math.floor(totalValue * 0.6); } function getBuildingCost(buildingType) { var count = 0; for (var i = 0; i < castleBuildings.length; i++) { if (castleBuildings[i].id === buildingType) { count++; } } var baseCost = BUILDING_DATA[buildingType] ? BUILDING_DATA[buildingType].cost : 0; var finalCost = { gold: Math.floor(baseCost * Math.pow(1.2, count)), people: 0 // Placeholder, if buildings cost people in the future }; return finalCost; } function setupStatsPanel() { isStatsPanelExpanded = false; statsPanel = new Container(); statsPanel.x = 0; statsPanel.y = -10; LK.gui.top.addChild(statsPanel); var panelBg = statsPanel.attachAsset('notification', { anchorX: 0.5, anchorY: 0, width: 2048, height: 70, tint: 0x222222, alpha: 0.8 }); statsPanel.hitAreaWidth = panelBg.width; statsPanel.hitAreaHeight = panelBg.height; var textY = 35; goldText = new Text2('Gold: 0', { size: 50, fill: 0xFFD700, weight: 800 }); goldText.anchor.set(0.5, 0.5); goldText.x = -400; goldText.y = textY; statsPanel.addChild(goldText); livesText = new Text2('Lives: 0', { size: 50, fill: 0x00FF00, weight: 800 }); livesText.anchor.set(0.5, 0.5); livesText.x = 400; livesText.y = textY; statsPanel.addChild(livesText); timerText = new Text2('', { // ZMIANA: Przeniesienie timerText na główny panel size: 45, fill: 0xFFD700, weight: 700 }); timerText.anchor.set(0.5, 0.5); timerText.x = 0; // ZMIANA: Pozycja na środku timerText.y = textY; timerText.visible = false; statsPanel.addChild(timerText); // ZMIANA: Dodanie do statsPanel, a nie do kontenera detali statsDetailsContainer = new Container(); statsDetailsContainer.y = 70; statsDetailsContainer.visible = false; statsPanel.addChild(statsDetailsContainer); var detailsTextOptions = { size: 45, fill: 0xFFFFFF, weight: 600, align: 'center' }; // ZMIANA: Lewa kolumna statystyk (pod Gold) peopleText = new Text2('People: 0/0', detailsTextOptions); peopleText.anchor.set(0.5, 0.5); peopleText.x = -400; peopleText.y = 40; statsDetailsContainer.addChild(peopleText); manaText = new Text2('Mana: 0', detailsTextOptions); manaText.anchor.set(0.5, 0.5); manaText.x = -400; manaText.y = 90; manaText.visible = false; statsDetailsContainer.addChild(manaText); // ZMIANA: Prawa kolumna statystyk (pod Lives) wavesText = new Text2('Wave: 0/0', detailsTextOptions); wavesText.anchor.set(0.5, 0.5); wavesText.x = 400; wavesText.y = 40; statsDetailsContainer.addChild(wavesText); enemiesText = new Text2('Enemies: 0/0', detailsTextOptions); enemiesText.anchor.set(0.5, 0.5); enemiesText.x = 400; enemiesText.y = 90; statsDetailsContainer.addChild(enemiesText); statsToggleButton = new Container(); statsToggleButton.x = panelBg.width / 2 - 500; statsToggleButton.y = 80; statsPanel.addChild(statsToggleButton); var arrow = statsToggleButton.attachAsset('player_arrow', { anchorX: 0.5, anchorY: 0.5, tint: 0xFFFFFF }); arrow.scale.set(1.5, 1.5); arrow.rotation = Math.PI / 2; statsToggleButton.down = function () { isStatsPanelExpanded = !isStatsPanelExpanded; var targetHeight = isStatsPanelExpanded ? 200 : 70; // ZMIANA: Dopasowanie wysokości var targetArrowRotation = isStatsPanelExpanded ? -Math.PI / 2 : Math.PI / 2; if (isStatsPanelExpanded) { statsDetailsContainer.visible = true; } tween(panelBg, { height: targetHeight }, { duration: 250, easing: tween.easeOut, onFinish: function onFinish() { if (!isStatsPanelExpanded) { statsDetailsContainer.visible = false; } } }); tween(arrow, { rotation: targetArrowRotation }, { duration: 250, easing: tween.easeOut }); }; } // --- FUNKCJE EKRANÓW --- function showTitleScreen() { if (titleScreenContainer) { titleScreenContainer.destroy(); } titleScreenContainer = new Container(); game.addChild(titleScreenContainer); currentScreenState = 'title'; if (LK.gui.top) { LK.gui.top.visible = false; } var bg = titleScreenContainer.attachAsset('title_background', {}); bg.scaleX = 1.1; bg.scaleY = 1.1; bg.x = -100; var gameTitleGraphic = titleScreenContainer.attachAsset('title_graphic', { anchorX: 0.5, anchorY: 0.5 }); gameTitleGraphic.x = 2048 / 2; gameTitleGraphic.y = 2732 / 2 - 400; gameTitleGraphic.alpha = 0; var startButton = titleScreenContainer.attachAsset('start_button_graphic', { anchorX: 0.5, anchorY: 0.5 }); startButton.x = 2048 / 2; startButton.y = 2732 / 2 + 200; startButton.interactive = true; startButton.alpha = 0; startButton.down = function () { gameTitleGraphic.destroy(); startButton.destroy(); showLevelSelectScreen(); }; tween(gameTitleGraphic, { alpha: 1 }, { duration: 2000, delay: 3000, easing: tween.easeIn }); tween(startButton, { alpha: 1 }, { duration: 1000, delay: 5000, easing: tween.easeIn }); function animateBg() { if (!bg.parent) { return; } tween(bg, { x: 0 }, { duration: 14000, easing: tween.easeInOut, onFinish: function onFinish() { tween(bg, { x: -100 }, { duration: 14000, easing: tween.easeInOut, onFinish: animateBg }); } }); } animateBg(); } function showLevelSelectScreen() { if (levelSelectContainer) { levelSelectContainer.destroy(); } levelSelectContainer = new Container(); game.addChild(levelSelectContainer); currentScreenState = 'levelSelect'; var parchment = levelSelectContainer.attachAsset('level_select_parchment', { anchorX: 0.5, anchorY: 0.5 }); parchment.x = 2048 / 2; parchment.y = 2732 / 2; parchment.alpha = 0; tween(parchment, { alpha: 1 }, { duration: 500 }); var selectTitle = new Text2("SELECT LEVEL", { size: 100, fill: 0x332211, weight: 700 }); selectTitle.anchor.set(0.5, 0.5); selectTitle.x = 2048 / 2; selectTitle.y = 350; levelSelectContainer.addChild(selectTitle); var levelKeys = Object.keys(LEVEL_DATA_ALL); var startY = 800; var buttonSpacing = 250; for (var i = 0; i < levelKeys.length; i++) { var levelKey = levelKeys[i]; var levelData = LEVEL_DATA_ALL[levelKey]; var levelButton = new Container(); levelButton.x = 2048 / 2; levelButton.y = startY + i * buttonSpacing; levelSelectContainer.addChild(levelButton); var levelButtonBg = levelButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 700, height: 180, tint: 0x0088CC }); var levelButtonText = new Text2(levelData.levelName, { size: 60, fill: 0xFFFFFF }); levelButtonText.anchor.set(0.5, 0.5); levelButton.addChild(levelButtonText); levelButton.interactive = true; (function (key) { levelButton.down = function () { if (titleScreenContainer) { titleScreenContainer.destroy(); } levelSelectContainer.destroy(); startGame(key); }; })(levelKey); } } function getPeopleRegenTime() { var hasInfirmary = castleBuildings.some(function (b) { return b.id === 'infirmary'; }); return hasInfirmary ? 5 * 60 : 10 * 60; } function toggleBuildPanel(panelOrState, stateOrPanel) { var panel, forceState; if (typeof panelOrState === 'string') { forceState = panelOrState; panel = stateOrPanel; } else { panel = panelOrState; forceState = stateOrPanel; } var targetY; var shouldBeOpen; if (!panel) { return; } if (forceState === 'open') { shouldBeOpen = true; } else if (forceState === 'close') { shouldBeOpen = false; } else { shouldBeOpen = !isBuildPanelOpen; } if (shouldBeOpen) { targetY = 2732 - 250; isBuildPanelOpen = true; } else { targetY = 2732 + 100; isBuildPanelOpen = false; } tween(panel, { y: targetY }, { duration: 300, easing: tween.backOut }); } function toggleMagicPanel(panel, forceState) { var targetY; var shouldBeOpen; if (!panel) { return; } if (forceState === 'open') { shouldBeOpen = true; } else if (forceState === 'close') { shouldBeOpen = false; } else { shouldBeOpen = !isMagicPanelOpen; } if (shouldBeOpen) { targetY = 2732 - 250; isMagicPanelOpen = true; } else { targetY = 2732 + 100; isMagicPanelOpen = false; } tween(panel, { y: targetY }, { duration: 300, easing: tween.backOut }); } function toggleArmyPanel(panel, forceState) { var targetY; var shouldBeOpen; if (!panel) { return; } if (forceState === 'open') { shouldBeOpen = true; } else if (forceState === 'close') { shouldBeOpen = false; } else { shouldBeOpen = !isArmyPanelOpen; } if (shouldBeOpen) { targetY = 2732 - 250; isArmyPanelOpen = true; } else { targetY = 2732 + 100; isArmyPanelOpen = false; isDraggingSpell = false; activeSpells = []; isDraggingSpell = false; } tween(panel, { y: targetY }, { duration: 300, easing: tween.backOut }); } function drawNewBuildingHand() { while (castleBuildPanel.children.length > 1) { castleBuildPanel.removeChildAt(1); } buildingHand = []; var availableBuildings = buildingDeck.filter(function (buildingType) { if (buildingType === 'home') { return true; } var alreadyBuilt = castleBuildings.some(function (b) { return b.id === buildingType; }); return !alreadyBuilt; }); var handToDraw = availableBuildings.slice(); for (var i = 0; i < 3; i++) { if (handToDraw.length === 0) { break; } var randomIndex = Math.floor(Math.random() * handToDraw.length); var drawnBuilding = handToDraw.splice(randomIndex, 1)[0]; buildingHand.push(drawnBuilding); } var buildingIconSpacing = 280; var totalBuildingWidth = (buildingHand.length - 1) * buildingIconSpacing; var buildingStartX = -totalBuildingWidth / 2; for (var i = 0; i < buildingHand.length; i++) { var building = new SourceBuilding(buildingHand[i]); building.x = buildingStartX + i * buildingIconSpacing; building.y = 0; castleBuildPanel.addChild(building); } } function placeBuilding(buildingType, gridX, gridY) { var cost = getBuildingCost(buildingType); var buildingSize = BUILDING_DATA[buildingType].size; if (gold >= cost.gold) { setGold(gold - cost.gold); var newBuilding = new Building(buildingType, buildingSize); newBuilding.x = castleGrid.x + gridX * CELL_SIZE; newBuilding.y = castleGrid.y + gridY * CELL_SIZE; castleBuildingsLayer.addChild(newBuilding); castleBuildings.push(newBuilding); for (var i = 0; i < buildingSize.w; i++) { for (var j = 0; j < buildingSize.h; j++) { var cell = castleGrid.getCell(gridX + i, gridY + j); if (cell) { cell.type = 5; } } } if (buildingType === 'home') { maxPeople += 5; updateUI(); } if (buildingType === 'magic_academy' && !isManaUnlocked) { isManaUnlocked = true; manaText.visible = true; magicButton.visible = true; updateUI(); } if (buildingType === 'barracks' && !isArmyUnlocked) { isArmyUnlocked = true; armyButton.visible = true; } var handIndex = buildingHand.indexOf(buildingType); if (handIndex > -1) { buildingHand.splice(handIndex, 1); } for (var i = castleBuildPanel.children.length - 1; i >= 0; i--) { var child = castleBuildPanel.children[i]; if (child.buildingType === buildingType) { child.destroy(); break; } } if (buildingHand.length === 0) { drawNewBuildingHand(); } return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } 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); tower.startConstructionAnimation(5, function () {}); return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } function initializeGameState(levelData) { pathId = 1; maxScore = 0; enemies = []; towers = []; bullets = []; friendlyUnits = []; peopleRegenQueue = []; activeSpells = []; selectedTower = null; castleBuildings = []; gold = levelData.initialGold; lives = levelData.initialLives; mana = 0; isManaUnlocked = false; isArmyUnlocked = false; enemiesKilled = 0; enemiesInCurrentWave = 0; currentWave = 1; totalWaves = levelData.waves.length; waveInProgress = false; waveSpawned = false; waveTimer = 0; nextWaveTime = 1200; sourceTowers = []; isBuildPanelOpen = false; isMagicPanelOpen = false; isArmyPanelOpen = false; people = 5; maxPeople = 5; currentActiveView = 'gate'; lastRecruitActionTick = 0; } function setupScene() { gateContainer = new Container(); game.addChild(gateContainer); castleContainer = new Container(); castleBuildingsLayer = new Container(); castleContainer.addChild(castleBuildingsLayer); game.addChild(castleContainer); castleContainer.visible = false; var castleMainBg = LK.getAsset('castle_bg_image', {}); castleContainer.addChildAt(castleMainBg, 0); castleMainBg.x = -100; castleMainBg.y = -300; gateContainer.addChild(LK.getAsset('map1', { scaleMode: 'nearest' })); debugLayer = new Container(); specialTilesLayer = new Container(); towerLayer = new Container(); friendlyUnitLayer = new Container(); buildableTilesLayer = new Container(); enemyLayerBottom = new Container(); enemyLayerMiddle = new Container(); enemyLayerTop = new Container(); enemyLayer = new Container(); enemyLayer.addChild(enemyLayerBottom); enemyLayer.addChild(enemyLayerMiddle); enemyLayer.addChild(enemyLayerTop); spellOverlay = new Container(); spellOverlay.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000 }); spellOverlay.alpha = 0; spellOverlay.visible = false; spellEffectLayer = new Container(); gateContainer.addChild(buildableTilesLayer); gateContainer.addChild(debugLayer); gateContainer.addChild(specialTilesLayer); gateContainer.addChild(towerLayer); gateContainer.addChild(friendlyUnitLayer); gateContainer.addChild(enemyLayer); gateContainer.addChild(spellOverlay); gateContainer.addChild(spellEffectLayer); buildableTilesLayer.visible = false; } function setupGrid(levelData) { grid = new Grid(24, 29 + 6); grid.x = 150; grid.y = 200 - CELL_SIZE * 4; for (var y = 0; y < levelData.mapLayout.length; y++) { for (var x = 0; x < levelData.mapLayout[y].length; x++) { var tileChar = levelData.mapLayout[y][x]; var cell = grid.getCell(x, y + 5); if (cell) { cell.specialEffect = null; if (tileChar === '0') { cell.type = 0; } else if (tileChar === '1' || tileChar === 'A' || tileChar === 'C') { cell.type = 1; if (tileChar === 'A') { cell.specialEffect = 'amplifier'; } if (tileChar === 'C') { cell.specialEffect = 'catalyst'; } var tilePlaceholder = buildableTilesLayer.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); tilePlaceholder.alpha = 0.25; tilePlaceholder.x = grid.x + x * CELL_SIZE + CELL_SIZE / 2; tilePlaceholder.y = grid.y + (y + 5) * CELL_SIZE + CELL_SIZE / 2; } else if (tileChar === 'S') { cell.type = 3; } else if (tileChar === 'E') { cell.type = 4; } else { cell.type = 2; } } } } var castleGridWidth = 17; var castleGridHeight = 22; castleGrid = new Grid(castleGridWidth, castleGridHeight); castleGrid.x = (2048 - castleGridWidth * CELL_SIZE) / 2 - 30; castleGrid.y = (2732 - castleGridHeight * CELL_SIZE) / 2 - 100; castleBuildableTilesLayer = new Container(); castleContainer.addChild(castleBuildableTilesLayer); for (var y = 0; y < levelData.castleMapLayout.length; y++) { for (var x = 0; x < levelData.castleMapLayout[y].length; x++) { var tileChar = levelData.castleMapLayout[y][x]; var cell = castleGrid.getCell(x, y); if (cell) { if (tileChar === '1') { cell.type = 1; var tilePlaceholder = castleBuildableTilesLayer.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); tilePlaceholder.alpha = 0.25; tilePlaceholder.x = castleGrid.x + x * CELL_SIZE + CELL_SIZE / 2; tilePlaceholder.y = castleGrid.y + y * CELL_SIZE + CELL_SIZE / 2; } else { cell.type = 2; } } } } } function setupUIAndPanels() { updateUI(); startGameButton = new Container(); var startBuildingBg = startGameButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 600, height: 150, tint: 0xFFD700 }); var startBuildingText = new Text2("Start Building", { size: 70, fill: 0x000000, weight: 'bold' }); startBuildingText.anchor.set(0.5, 0.5); startGameButton.addChild(startBuildingText); startGameButton.x = 2048 / 2; startGameButton.y = 2732 / 2; gateContainer.addChild(startGameButton); startGameButton.interactive = true; startGameButton.down = function () { isBuildPhase = true; buildPhaseTimer = 60 * 20; timerText.visible = true; startGameButton.destroy(); }; buildPanelContainer = new Container(); gateContainer.addChild(buildPanelContainer); buildPanelContainer.x = 2048 / 2; buildPanelContainer.y = 2732 + 100; var panelBackground = buildPanelContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 1800, height: 400 }); panelBackground.tint = 0x222222; panelBackground.alpha = 0.9; var towerTypes = ['guard', 'crossbow', 'mage', 'banner']; var towerIconSpacing = 280; var totalWidth = (towerTypes.length - 1) * towerIconSpacing; var towerStartX = -totalWidth / 2; for (var i = 0; i < towerTypes.length; i++) { var tower = new SourceTower(towerTypes[i]); tower.x = towerStartX + i * towerIconSpacing; tower.y = 0; buildPanelContainer.addChild(tower); sourceTowers.push(tower); } castleBuildPanel = new Container(); castleContainer.addChild(castleBuildPanel); castleBuildPanel.x = 2048 / 2; castleBuildPanel.y = 2732 + 100; var castlePanelBackground = castleBuildPanel.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 1800, height: 400 }); castlePanelBackground.tint = 0x332211; castlePanelBackground.alpha = 0.9; magicPanelContainer = new Container(); gateContainer.addChild(magicPanelContainer); magicPanelContainer.x = 2048 / 2; magicPanelContainer.y = 2732 + 100; var magicPanelBackground = magicPanelContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 1800, height: 400 }); magicPanelBackground.tint = 0x1A237E; magicPanelBackground.alpha = 0.9; var spellTypes = Object.keys(SPELL_DATA); var spellIconSpacing = 280; var totalSpellsWidth = (spellTypes.length - 1) * spellIconSpacing; var spellStartX = -totalSpellsWidth / 2; for (var i = 0; i < spellTypes.length; i++) { var spell = new SourceSpell(spellTypes[i]); spell.x = spellStartX + i * spellIconSpacing; spell.y = 0; magicPanelContainer.addChild(spell); } armyPanelContainer = new Container(); gateContainer.addChild(armyPanelContainer); armyPanelContainer.x = 2048 / 2; armyPanelContainer.y = 2732 + 100; var armyPanelBackground = armyPanelContainer.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 1800, height: 400 }); armyPanelBackground.tint = 0x613218; armyPanelBackground.alpha = 0.9; var unitTypes = Object.keys(UNIT_DATA); var unitIconSpacing = 280; var totalUnitsWidth = (unitTypes.length - 1) * unitIconSpacing; var unitStartX = -totalUnitsWidth / 2; for (var i = 0; i < unitTypes.length; i++) { var unit = new SourceUnit(unitTypes[i]); unit.x = unitStartX + i * unitIconSpacing; unit.y = 0; armyPanelContainer.addChild(unit); } buildButton = new Container(); game.addChild(buildButton); buildButton.interactive = true; buildButton.x = 2048 / 2 - 400; buildButton.y = 2732 - 100; var buildButtonBg = buildButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 300, height: 120 }); buildButtonBg.tint = 0xFFD700; var buildButtonText = new Text2("Build", { size: 60, fill: 0x000000, weight: 800 }); buildButtonText.anchor.set(0.5, 0.5); buildButton.addChild(buildButtonText); buildButton.down = function () { if (currentActiveView === 'gate') { toggleBuildPanel(buildPanelContainer); toggleMagicPanel(magicPanelContainer, 'close'); toggleArmyPanel(armyPanelContainer, 'close'); } else { toggleBuildPanel(castleBuildPanel); } }; magicButton = new Container(); game.addChild(magicButton); magicButton.interactive = true; magicButton.x = 2048 / 2; magicButton.y = 2732 - 100; magicButton.visible = false; var magicButtonBg = magicButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 300, height: 120 }); magicButtonBg.tint = 0x00BFFF; var magicButtonText = new Text2("Magic", { size: 60, fill: 0x000000, weight: 800 }); magicButtonText.anchor.set(0.5, 0.5); magicButton.addChild(magicButtonText); magicButton.down = function () { if (currentActiveView === 'gate') { toggleMagicPanel(magicPanelContainer); toggleBuildPanel(buildPanelContainer, 'close'); toggleArmyPanel(armyPanelContainer, 'close'); } }; armyButton = new Container(); game.addChild(armyButton); armyButton.interactive = true; armyButton.x = 2048 / 2 + 400; armyButton.y = 2732 - 100; armyButton.visible = false; var armyButtonBg = armyButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 300, height: 120 }); armyButtonBg.tint = 0xFF8C00; var armyButtonText = new Text2("Army", { size: 60, fill: 0x000000, weight: 800 }); armyButtonText.anchor.set(0.5, 0.5); armyButton.addChild(armyButtonText); armyButton.down = function () { if (currentActiveView === 'gate') { toggleArmyPanel(armyPanelContainer); toggleBuildPanel(buildPanelContainer, 'close'); toggleMagicPanel(magicPanelContainer, 'close'); } }; viewToggleButton = new Container(); game.addChild(viewToggleButton); viewToggleButton.interactive = true; viewToggleButton.x = 250; viewToggleButton.y = 2732 - 100; var viewToggleButtonBg = viewToggleButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 300, height: 120, tint: 0x4466AA }); var viewToggleButtonText = new Text2("Castle", { size: 60, fill: 0xFFFFFF, weight: 800 }); viewToggleButtonText.anchor.set(0.5, 0.5); viewToggleButton.addChild(viewToggleButtonText); viewToggleButton.down = function () { if (currentActiveView === 'gate') { currentActiveView = 'castle'; viewToggleButtonText.setText("Gate"); rerollButton.visible = true; gateContainer.visible = false; castleContainer.visible = true; } else { currentActiveView = 'gate'; viewToggleButtonText.setText("Castle"); rerollButton.visible = false; gateContainer.visible = true; castleContainer.visible = false; } }; rerollButton = new Container(); game.addChild(rerollButton); rerollButton.interactive = true; rerollButton.x = 2048 - 250; rerollButton.y = 2732 - 100; rerollButton.visible = false; var rerollButtonBg = rerollButton.attachAsset('notification', { anchorX: 0.5, anchorY: 0.5, width: 300, height: 120, tint: 0xCC0000 }); var rerollButtonText = new Text2("Re-roll", { size: 60, fill: 0xFFFFFF, weight: 800 }); rerollButtonText.anchor.set(0.5, 0.5); rerollButton.addChild(rerollButtonText); rerollButton.down = function () { if (currentActiveView === 'castle') { drawNewBuildingHand(); } }; placementPreview = new PlacementPreview(); game.addChild(placementPreview); placementPreview.visible = false; spellPreview = new SpellPreview(); game.addChild(spellPreview); spellPreview.visible = false; buildingDeck = ['home', 'workshop', 'barracks', 'magic_academy', 'tavern', 'smithy', 'infirmary']; buildingHand = []; drawNewBuildingHand(); } function handleUIClick(x, y) { var buttons = [{ btn: buildButton, visible: true }, { btn: magicButton, visible: magicButton.visible }, { btn: armyButton, visible: armyButton.visible }, { btn: viewToggleButton, visible: true }, { btn: rerollButton, visible: rerollButton.visible }]; for (var i = 0; i < buttons.length; i++) { var b = buttons[i]; if (!b.visible) { continue; } // Nowa, niezawodna metoda sprawdzania granic var width = b.btn.hitAreaWidth; var height = b.btn.hitAreaHeight; var left = b.btn.x - width / 2; var right = b.btn.x + width / 2; var top = b.btn.y - height / 2; var bottom = b.btn.y + height / 2; if (x >= left && x <= right && y >= top && y <= bottom) { b.btn.down(); return true; } } return false; } function startGame(levelNumber) { console.log("Rozpoczynam ładowanie poziomu: " + levelNumber); if (titleScreenContainer) { titleScreenContainer.destroy(); titleScreenContainer = null; } if (levelSelectContainer) { levelSelectContainer.destroy(); levelSelectContainer = null; } currentScreenState = 'gameplay'; if (LK.gui.top) { LK.gui.top.visible = true; } levelData = LEVEL_DATA_ALL[levelNumber]; if (!levelData) { console.error("Nie znaleziono danych dla poziomu: " + levelNumber); return; } initializeGameState(levelData); setupScene(); setupGrid(levelData); if (levelData.spawnPoints) { levelData.spawnPoints.forEach(function (spawner) { // --- POCZĄTEK POPRAWKI --- // Znajdź wszystkie komórki końcowe na szerokości ścieżki var goalCells = []; for (var i = -1; i <= 1; i++) { // Sprawdzamy środek, lewo i prawo od punktu docelowego var cell = grid.getCell(spawner.goalX + i, spawner.goalY); // Upewniamy się, że komórka istnieje i jest częścią ścieżki (type 0) if (cell && cell.type === 0) { goalCells.push(cell); } } // Jeśli znaleziono jakiekolwiek komórki docelowe, oblicz ścieżkę do nich if (goalCells.length > 0) { grid.calculatePath(spawner.id, goalCells); } // --- KONIEC POPRAWKI --- }); } setupStatsPanel(); setupUIAndPanels(); } function handlePanelClick(x, y) { var aPanelIsOpen = isBuildPanelOpen || isMagicPanelOpen || isArmyPanelOpen; if (!aPanelIsOpen) { return false; } var activePanel; var itemTypes; if (isBuildPanelOpen) { activePanel = currentActiveView === 'gate' ? buildPanelContainer : castleBuildPanel; itemTypes = [SourceTower, SourceBuilding]; } else if (isMagicPanelOpen) { activePanel = magicPanelContainer; itemTypes = [SourceSpell]; } else if (isArmyPanelOpen) { activePanel = armyPanelContainer; itemTypes = [SourceUnit]; } var localClickPos = { x: x - activePanel.x, y: y - activePanel.y }; var sourceItems = activePanel.children.filter(function (c) { for (var i = 0; i < itemTypes.length; i++) { if (c instanceof itemTypes[i]) { return true; } } return false; }); for (var i = 0; i < sourceItems.length; i++) { var item = sourceItems[i]; var itemLeft = item.x - item.width / 2; var itemRight = item.x + item.width / 2; var itemTop = item.y - item.height / 2; var itemBottom = item.y + item.height / 2; if (localClickPos.x >= itemLeft && localClickPos.x <= itemRight && localClickPos.y >= itemTop && localClickPos.y <= itemBottom) { if (item instanceof SourceUnit) { item.down(); return true; } if (item instanceof SourceTower || item instanceof SourceBuilding) { isDragging = true; placementPreview.visible = true; var activeGrid = currentActiveView === 'gate' ? grid : castleGrid; var itemSubType = item.towerType || item.buildingType; placementPreview.itemType = item.towerType ? 'tower' : 'building'; placementPreview.subType = itemSubType; if (placementPreview.itemType === 'tower') { placementPreview.itemSize = { w: 2, h: 2 }; } else { placementPreview.itemSize = BUILDING_DATA[itemSubType].size; } placementPreview.updateAppearance(); if (currentActiveView === 'gate') { buildableTilesLayer.visible = true; } else { castleBuildableTilesLayer.visible = true; } placementPreview.snapToGrid(x, y, activeGrid); return true; } if (item instanceof SourceSpell) { var spellData = SPELL_DATA[item.spellId]; if (mana >= spellData.cost) { isDraggingSpell = true; spellPreview.spellId = item.spellId; spellPreview.updateAppearance(spellData, true); spellPreview.updatePosition(x, y); showSpellOverlay(); } else { var notification = game.addChild(new Notification("Not enough mana!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } return true; } } } return false; } function handleClickOnMap(x, y) { if (rerollButton.visible) { var width = 300; var height = 120; var left = rerollButton.x - width / 2; var right = rerollButton.x + width / 2; var top = rerollButton.y - height / 2; var bottom = rerollButton.y + height / 2; if (x >= left && x <= right && y >= top && y <= bottom) { return; } } if (isMenuTransitioning) { return; } var upgradeMenuVisible = game.children.some(function (child) { return child instanceof UpgradeMenu || child instanceof BuildingInfoMenu; }); if (upgradeMenuVisible) { return; } var aPanelIsOpen = isBuildPanelOpen || isMagicPanelOpen || isArmyPanelOpen; if (aPanelIsOpen) { var activePanel; if (isBuildPanelOpen) { activePanel = currentActiveView === 'gate' ? buildPanelContainer : castleBuildPanel; } else if (isMagicPanelOpen) { activePanel = magicPanelContainer; } else if (isArmyPanelOpen) { activePanel = armyPanelContainer; } if (activePanel && !activePanel.getBounds().contains(x, y)) { if (isBuildPanelOpen) { toggleBuildPanel(activePanel, 'close'); } if (isMagicPanelOpen) { toggleMagicPanel(magicPanelContainer, 'close'); } if (isArmyPanelOpen) { toggleArmyPanel(armyPanelContainer, 'close'); } return; } } var clickedOnSomething = false; if (currentActiveView === 'gate') { for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerBounds = tower.getBounds(); if (towerBounds.contains(x - tower.x + tower.width / 2, y - tower.y + tower.height / 2)) { tower.down(x, y); clickedOnSomething = true; break; } } } else { for (var i = 0; i < castleBuildings.length; i++) { var building = castleBuildings[i]; var buildingWidth = building.size.w * CELL_SIZE; var buildingHeight = building.size.h * CELL_SIZE; if (x > building.x && x < building.x + buildingWidth && y > building.y && y < building.y + buildingHeight) { if (typeof building.down === 'function') { building.down(); } clickedOnSomething = true; break; } } } if (!clickedOnSomething) { closeActiveUpgradeMenu(); game.children.forEach(function (child) { if (child instanceof BuildingInfoMenu) { hideBuildingInfoMenu(child); } }); } } function handleItemDrop(x, y) { if (isDragging) { isDragging = false; placementPreview.visible = false; if (currentActiveView === 'gate') { buildableTilesLayer.visible = false; if (placementPreview.canPlace) { placeTower(placementPreview.gridX, placementPreview.gridY, placementPreview.subType); } } else { castleBuildableTilesLayer.visible = false; if (placementPreview.canPlace) { placeBuilding(placementPreview.subType, placementPreview.gridX, placementPreview.gridY); } } if (isBuildPanelOpen) { var targetPanel = currentActiveView === 'gate' ? buildPanelContainer : castleBuildPanel; toggleBuildPanel(targetPanel, 'close'); } return true; } return false; } function handleSpellCast(x, y) { if (isDraggingSpell) { isDraggingSpell = false; spellPreview.visible = false; spellPreview.children[0].tint = 0x00BFFF; LK.setTimeout(function () { hideSpellOverlay(); }, 1500); var spellData = SPELL_DATA[spellPreview.spellId]; if (mana >= spellData.cost) { var spellEffect; if (spellPreview.spellId === 'summon_golem') { var gridX = Math.floor((x - grid.x) / CELL_SIZE); var gridY = Math.floor((y - grid.y) / CELL_SIZE); var cell = grid.getCell(gridX, gridY); if (cell && cell.type === 0) { setMana(mana - spellData.cost); var golemX = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2; var golemY = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2; var golemUnit = new Golem(golemX, golemY); golemUnit.visible = false; friendlyUnitLayer.addChild(golemUnit); friendlyUnits.push(golemUnit); var summonEffect = new GolemSummonEffect(golemX, golemY, function () { golemUnit.visible = true; }); spellEffectLayer.addChild(summonEffect); } else { var notification = game.addChild(new Notification("Cannot summon here!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } else { setMana(mana - spellData.cost); switch (spellPreview.spellId) { case 'fire_wall': spellEffect = new FireWall(x, y, spellData); break; case 'lightning_stun': spellEffect = new ThunderstormEffect(x, y, spellData); break; case 'healing_field': spellEffect = new HealingFieldEffect(x, y, spellData); break; case 'curse': spellEffect = new CurseEffect(x, y, spellData); break; } if (spellEffect) { if (spellPreview.spellId === 'lightning_stun') { game.addChild(spellEffect); } else { spellEffectLayer.addChild(spellEffect); } if (spellPreview.spellId === 'fire_wall') { activeSpells.push(spellEffect); } } } } if (isMagicPanelOpen) { toggleMagicPanel(magicPanelContainer, 'close'); } return true; } return false; } game.down = function (x, y, obj) { if (currentScreenState !== 'gameplay') { if (obj && obj.down) { obj.down(); } return; } if (obj && typeof obj.down === 'function') { obj.down(); return; } if (handlePanelClick(x, y)) { return; } handleClickOnMap(x, y); }; game.move = function (x, y, obj) { if (currentScreenState !== 'gameplay') { return; } if (isDragging) { var activeGrid = currentActiveView === 'gate' ? grid : castleGrid; var yOffset = currentActiveView === 'gate' ? CELL_SIZE * 1.5 : 0; placementPreview.snapToGrid(x, y - yOffset, activeGrid); } else if (isDraggingSpell) { spellPreview.updatePosition(x, y); if (spellPreview.spellId === 'summon_golem') { var gridX = Math.floor((x - grid.x) / CELL_SIZE); var gridY = Math.floor((y - grid.y) / CELL_SIZE); var cell = grid.getCell(gridX, gridY); var canPlace = cell && cell.type === 0; var activeGraphic = spellPreview.getActiveGraphic(); if (activeGraphic) { activeGraphic.tint = canPlace ? 0x00BFFF : 0xFF0000; } } } }; game.up = function (x, y, obj) { if (currentScreenState !== 'gameplay') { return; } if (panelActionTaken) { panelActionTaken = false; return; } if (handleItemDrop(x, y)) { return; } if (handleSpellCast(x, y)) { return; } if (isDraggingSpell) { isDraggingSpell = false; spellPreview.visible = false; hideSpellOverlay(); } var clickedOnMenu = false; var upgradeMenus = game.children.filter(function (child) { return child instanceof UpgradeMenu; }); if (upgradeMenus.length > 0) { for (var i = 0; i < upgradeMenus.length; i++) { var menu = upgradeMenus[i]; var menuBounds = menu.getBounds(); if (menuBounds.contains(x, y)) { clickedOnMenu = true; break; } } } if (!clickedOnMenu && selectedTower) { closeActiveUpgradeMenu(); } }; function updateGameLogic() { if (isBuildPhase) { if (buildPhaseTimer > 0) { buildPhaseTimer--; timerText.setText("Build Time: " + Math.ceil(buildPhaseTimer / 60)); } else { isBuildPhase = false; timerText.visible = false; waveInProgress = true; waveSpawned = false; } } else if (waveInProgress) { if (!waveSpawned) { spawnQueue = []; waveClock = 0; enemiesKilled = 0; var waveData = levelData.waves[currentWave - 1]; if (waveData && waveData.phases) { var allEnemiesInWave = []; var currentTimeOffset = 0; var globalUnitIndex = 0; waveData.phases.forEach(function (phase) { currentTimeOffset += phase.delay || 0; var timeInThisPhase = 0; phase.units.forEach(function (unitGroup) { var spawner = levelData.spawnPoints.find(function (sp) { return sp.id === unitGroup.spawner; }); if (!spawner || spawner.lanes <= 0) { return; } // --- POCZĄTEK NOWEJ LOGIKI PAROWANIA --- // Pętla iteruje teraz co liczbę rzędów (czyli co parę) for (var i = 0; i < unitGroup.count; i += spawner.lanes) { var pairIndex = i / spawner.lanes; var spawnTimeForPair = currentTimeOffset + pairIndex * (phase.interval || 120); // Ta pętla tworzy jednostki w parze (lub pojedynczo, jeśli 1 rząd) for (var j = 0; j < spawner.lanes; j++) { if (i + j < unitGroup.count) { // Zabezpieczenie dla nieparzystej liczby wrogów spawnQueue.push({ unitData: { spawner: unitGroup.spawner, type: unitGroup.type, isBoss: unitGroup.isBoss || false }, spawnTime: spawnTimeForPair, // Ten sam czas dla całej pary unitIndex: globalUnitIndex }); allEnemiesInWave.push(unitGroup.type); globalUnitIndex++; } } } // --- KONIEC NOWEJ LOGIKI PAROWANIA --- timeInThisPhase += Math.ceil(unitGroup.count / spawner.lanes) * (phase.interval || 120); }); currentTimeOffset += timeInThisPhase; }); enemiesInCurrentWave = allEnemiesInWave.length; updateUI(); } waveSpawned = true; } waveClock++; if (spawnQueue.length > 0 && waveClock >= spawnQueue[0].spawnTime) { var spawnData = spawnQueue.shift(); var spawner = levelData.spawnPoints.find(function (sp) { return sp.id === spawnData.unitData.spawner; }); if (spawner) { var enemy = new Enemy(spawnData.unitData.type, spawnData.unitData.spawner); enemy.laneIndex = spawnData.unitIndex % spawner.lanes; var laneOffset = (enemy.laneIndex - (spawner.lanes - 1) / 2) * spawner.laneOffset; enemy.currentCellX = spawner.startX + laneOffset; enemy.currentCellY = spawner.startY; enemy.cellX = spawner.startX; enemy.cellY = spawner.startY; enemy.x = grid.x + enemy.currentCellX * CELL_SIZE; enemy.y = grid.y + enemy.currentCellY * CELL_SIZE; enemy.waveNumber = currentWave; if (spawnData.unitData.isBoss) { enemy.isBoss = true; } var healthMultiplier = Math.pow(1.15, currentWave); enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); enemy.health = enemy.maxHealth; if (enemy.isFlying) { enemyLayerTop.addChild(enemy); if (enemy.shadow) { enemyLayerMiddle.addChild(enemy.shadow); } } else { enemyLayerBottom.addChild(enemy); } enemies.push(enemy); } } if (spawnQueue.length === 0 && enemies.length === 0) { waveInProgress = false; if (numberOfTaverns > 0) { var baseTavernIncome = numberOfTaverns * 30; var populationBonus = people * 2; var totalTavernIncome = baseTavernIncome + populationBonus; setGold(gold + totalTavernIncome); var notification = game.addChild(new Notification("+" + totalTavernIncome + "g from Taverns")); notification.x = 2048 / 2; notification.y = grid.height - 150; } currentWave++; if (currentWave > totalWaves) { LK.showYouWin(); } else { isBuildPhase = true; buildPhaseTimer = 60 * 10; } } } } function updateGameObjects() { if (friendlyUnits) { for (var i = 0; i < friendlyUnits.length; i++) { friendlyUnits[i].update(); } } if (activeSpells) { for (var i = activeSpells.length - 1; i >= 0; i--) { if (activeSpells[i]) { activeSpells[i].update(); } } } if (peopleRegenQueue) { for (var i = peopleRegenQueue.length - 1; i >= 0; i--) { var regenTime = getPeopleRegenTime(); if (LK.ticks - peopleRegenQueue[i] >= regenTime) { if (people < maxPeople) { people++; updateUI(); } peopleRegenQueue.splice(i, 1); } } } for (var i = 0; i < towers.length; i++) { towers[i].update(); } for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; if (!bullet.parent) { bullets.splice(i, 1); } else { bullet.update(); } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { enemiesKilled++; updateUI(); var goldEarned = enemy.goldValue || 0; setGold(gold + goldEarned); if (isManaUnlocked) { var manaEarned = 0; switch (enemy.manaCategory) { case 'weak': manaEarned = 1; break; case 'strong': manaEarned = 2; break; case 'special': manaEarned = 5; break; case 'boss': manaEarned = 25; break; } setMana(mana + manaEarned); } gateContainer.addChild(new GoldIndicator(goldEarned, enemy.x, enemy.y)); if (enemy.parent) { enemy.parent.removeChild(enemy); } if (enemy.shadow && enemy.shadow.parent) { enemy.shadow.parent.removeChild(enemy.shadow); } enemies.splice(a, 1); continue; } if (enemy.update()) { if (enemy.parent) { enemy.parent.removeChild(enemy); } if (enemy.shadow && enemy.shadow.parent) { enemy.shadow.parent.removeChild(enemy.shadow); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { LK.showGameOver(); } } } towerLayer.children.sort(function (a, b) { return a.y - b.y; }); friendlyUnitLayer.children.sort(function (a, b) { return a.y - b.y; }); enemyLayerBottom.children.sort(function (a, b) { return a.y - b.y; }); } function updatePreviews() { if (placementPreview.visible) { var activeGrid = currentActiveView === 'gate' ? grid : castleGrid; placementPreview.updatePlacementStatus(activeGrid); } } game.update = function () { if (currentScreenState !== 'gameplay') { return; } updateGameLogic(); updateGameObjects(); updatePreviews(); }; showTitleScreen();
===================================================================
--- original.js
+++ change.js
@@ -495,11 +495,13 @@
}
});
return self;
});
-var Enemy = Container.expand(function (type) {
+var Enemy = Container.expand(function (type, spawnerId) {
var self = Container.call(this);
self.type = type || 'normal';
+ self.spawnerId = spawnerId;
+ self.laneIndex = 0;
self.speed = 0.01;
self.originalSpeed = self.speed;
self.slowDuration = 0;
self.slowFactor = 1;
@@ -538,39 +540,39 @@
case 'shield':
self.maxHealth = 94;
self.damage = 3;
self.manaCategory = 'strong';
- self.goldValue = 30;
+ self.goldValue = 36;
break;
case 'rider':
self.maxHealth = 78;
self.damage = 6;
self.speed *= 1.5;
self.manaCategory = 'strong';
- self.goldValue = 49;
+ self.goldValue = 59;
break;
case 'chieftain':
self.maxHealth = 524;
self.damage = 10;
self.isBoss = true;
self.manaCategory = 'boss';
- self.goldValue = 253;
+ self.goldValue = 304;
break;
case 'archer':
self.maxHealth = 31;
self.damage = 7;
self.attackSpeed = 48;
self.isRanged = true;
self.range = 6 * CELL_SIZE;
self.manaCategory = 'strong';
- self.goldValue = 28;
+ self.goldValue = 34;
break;
case 'normal':
default:
self.maxHealth = 62;
self.damage = 4;
self.manaCategory = 'weak';
- self.goldValue = 16;
+ self.goldValue = 19;
break;
}
self.originalSpeed = self.speed;
self.health = self.maxHealth;
@@ -583,24 +585,8 @@
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
- if (self.isFlying) {
- self.shadow = new Container();
- var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- shadowGraphics.tint = 0x000000;
- shadowGraphics.alpha = 0.4;
- if (self.isBoss) {
- shadowGraphics.scaleX = 1.8;
- shadowGraphics.scaleY = 1.8;
- }
- self.shadow.x = 20;
- self.shadow.y = 20;
- shadowGraphics.rotation = enemyGraphics.rotation;
- }
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
@@ -722,8 +708,18 @@
} else {
self.speed *= self.slowFactor;
}
}
+ if (self.isBurning) {
+ if (LK.ticks - self.lastBurnTick >= 60) {
+ self.takeDamage(self.burnDamage);
+ self.lastBurnTick = LK.ticks;
+ }
+ self.burnDuration--;
+ if (self.burnDuration <= 0) {
+ self.isBurning = false;
+ }
+ }
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
return false;
@@ -766,17 +762,33 @@
self.y += moveY / moveMagnitude * self.combatSpeed;
}
}
} else {
- if (grid.updateEnemy(self)) {
- return true;
- }
- 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);
+ var currentCell = grid.getCell(Math.round(self.currentCellX), Math.round(self.currentCellY));
+ if (currentCell && currentCell.targets[self.spawnerId]) {
+ if (grid.updateEnemy(self)) {
+ return true;
}
+ } else {
+ var recoveryTarget = null;
+ var minRecoveryDist = Infinity;
+ for (var i = 0; i < grid.cells.length; i++) {
+ for (var j = 0; j < grid.cells[i].length; j++) {
+ var cell = grid.cells[i][j];
+ if (cell.targets[self.spawnerId]) {
+ var distSq = Math.pow(self.x - (grid.x + i * CELL_SIZE), 2) + Math.pow(self.y - (grid.y + j * CELL_SIZE), 2);
+ if (distSq < minRecoveryDist) {
+ minRecoveryDist = distSq;
+ recoveryTarget = cell;
+ }
+ }
+ }
+ }
+ if (recoveryTarget) {
+ var angle = Math.atan2(grid.y + recoveryTarget.y * CELL_SIZE - self.y, grid.x + recoveryTarget.x * CELL_SIZE - self.x);
+ self.x += Math.cos(angle) * self.combatSpeed;
+ self.y += Math.sin(angle) * self.combatSpeed;
+ }
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
return false;
@@ -797,9 +809,9 @@
frameSprite.visible = false;
allFrames.push(frameSprite);
}
var currentFrame = 0;
- var frameDuration = 4;
+ var frameDuration = 5;
var frameTicker = 0;
if (allFrames.length > 0) {
allFrames[currentFrame].visible = true;
}
@@ -1048,11 +1060,8 @@
}
if (enemies.length === 0) {
return;
}
- if (self.isRanged) {
- return;
- }
var gridX = Math.floor((self.x - grid.x) / CELL_SIZE);
var gridY = Math.floor((self.y - grid.y) / CELL_SIZE);
var currentCell = grid.getCell(gridX, gridY);
var targetCell = null;
@@ -1213,8 +1222,15 @@
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
+ self.takeDamage = function (amount) {
+ self.health -= amount;
+ if (self.health < 0) {
+ self.health = 0;
+ }
+ self.healthBar.width = self.health / self.maxHealth * 70;
+ };
self.findClosestEnemy = function () {
var closestEnemy = null;
var minDistanceSq = self.range * self.range;
for (var i = 0; i < enemies.length; i++) {
@@ -1331,18 +1347,16 @@
});
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,
- aggressionScore: 0,
- aggressionPathId: 0,
+ targets: {},
+ score: {},
+ aggressionTargets: {},
+ aggressionScore: {},
towersInRange: [],
specialEffect: null
};
}
@@ -1351,197 +1365,86 @@
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
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 = [];
- cell.aggressionTargets = [];
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
- self.pathFind = function () {
- var before = new Date().getTime();
- self.goals = [];
- for (var i = 0; i < gridWidth; i++) {
- for (var j = 0; j < gridHeight; j++) {
- if (self.cells[i][j].type === 4) {
- self.goals.push(self.cells[i][j]);
- }
- }
- }
- var toProcess = self.goals.concat([]);
- maxScore = 0;
- pathId += 1;
+ self.calculatePath = function (pathId, goalCell) {
+ var toProcess = [goalCell];
+ var pathCounter = Math.random();
for (var a = 0; a < toProcess.length; a++) {
- toProcess[a].pathId = pathId;
- toProcess[a].score = 0;
+ toProcess[a].pathId = pathCounter;
+ toProcess[a].score[pathId] = 0;
}
+ var maxScore = 0;
function processNode(node, targetValue, targetNode) {
- if (node && (node.type === 0 || node.type === 3 || node.type === 4)) {
- 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) {
+ if (node && (node.type === 0 || node.type === 4)) {
+ if (!node.score[pathId] || targetValue < node.score[pathId]) {
+ node.targets[pathId] = [targetNode];
+ node.score[pathId] = targetValue;
+ if (node.pathId != pathCounter) {
toProcess.push(node);
+ node.pathId = pathCounter;
}
- node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
+ } else if (targetValue === node.score[pathId]) {
+ node.targets[pathId].push(targetNode);
}
}
}
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 === 0 || node.up.type === 3) && (node.left.type === 0 || node.left.type === 3)) {
- processNode(node.upLeft, targetScore, node);
- }
- if (node.up && node.right && (node.up.type === 0 || node.up.type === 3) && (node.right.type === 0 || node.right.type === 3)) {
- processNode(node.upRight, targetScore, node);
- }
- if (node.down && node.right && (node.down.type === 0 || node.down.type === 3) && (node.right.type === 0 || node.right.type === 3)) {
- processNode(node.downRight, targetScore, node);
- }
- if (node.down && node.left && (node.down.type === 0 || node.down.type === 3) && (node.left.type === 0 || node.left.type === 3)) {
- processNode(node.downLeft, targetScore, node);
- }
- targetScore = node.score + 10000;
+ var targetScore = node.score[pathId] + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
- self.spawns = [];
- for (var i = 0; i < gridWidth; i++) {
- for (var j = 0; j < gridHeight; j++) {
- if (self.cells[i][j].type === 3) {
- self.spawns.push(self.cells[i][j]);
- }
- }
- }
- for (var a = 0; a < self.spawns.length; a++) {
- if (self.spawns[a].pathId != pathId) {
- return true;
- }
- }
- for (var a = 0; a < enemies.length; a++) {
- var enemy = enemies[a];
- 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) {
- return true;
- }
- }
- } else if (!target || target.pathId != pathId) {
- return true;
- }
- }
};
- self.calculateAggressionPath = function () {
- var toProcess = self.spawns.concat([]);
- var aggressionPathId = pathId + 1;
- for (var a = 0; a < toProcess.length; a++) {
- toProcess[a].aggressionPathId = aggressionPathId;
- toProcess[a].aggressionScore = 0;
- }
- function processAggressionNode(node, targetValue, targetNode) {
- if (node && (node.type === 0 || node.type === 3 || node.type === 4)) {
- if (node.aggressionPathId < aggressionPathId || targetValue < node.aggressionScore) {
- node.aggressionTargets = [targetNode];
- } else if (node.aggressionPathId == aggressionPathId && targetValue == node.aggressionScore) {
- node.aggressionTargets.push(targetNode);
- }
- if (node.aggressionPathId < aggressionPathId || targetValue < node.aggressionScore) {
- node.aggressionScore = targetValue;
- if (node.aggressionPathId != aggressionPathId) {
- toProcess.push(node);
- }
- node.aggressionPathId = aggressionPathId;
- }
- }
- }
- while (toProcess.length) {
- var nodes = toProcess;
- toProcess = [];
- for (var a = 0; a < nodes.length; a++) {
- var node = nodes[a];
- var targetScore = node.aggressionScore + 10000;
- processAggressionNode(node.up, targetScore, node);
- processAggressionNode(node.right, targetScore, node);
- processAggressionNode(node.down, targetScore, node);
- processAggressionNode(node.left, targetScore, node);
- }
- }
- };
self.renderDebug = function () {};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
- if (cell && cell.type == 4) {
+ if (cell && cell.type === 4) {
return true;
}
- if (enemy.isFlying && enemy.shadow) {
- enemy.shadow.x = enemy.x + 20;
- enemy.shadow.y = enemy.y + 20;
- if (enemy.children[0] && enemy.shadow.children[0]) {
- enemy.shadow.children[0].rotation = enemy.children[0].rotation;
- }
- }
if (enemy.isFlying) {
- if (!enemy.flyingTarget) {
- enemy.flyingTarget = self.goals[Math.floor(Math.random() * self.goals.length)];
- }
- 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) {
- return true;
- }
- 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;
- enemy.cellX = Math.round(enemy.currentCellX);
- enemy.cellY = Math.round(enemy.currentCellY);
+ // Logika dla latajacych zostaje uproszczona, mozna rozbudowac
return false;
}
if (!enemy.currentTarget) {
- if (cell) {
- enemy.currentTarget = cell.targets[Math.floor(Math.random() * cell.targets.length)];
+ if (cell && cell.targets[enemy.spawnerId]) {
+ var targets = cell.targets[enemy.spawnerId];
+ enemy.currentTarget = targets[Math.floor(Math.random() * targets.length)];
}
}
if (enemy.currentTarget) {
- var ox = enemy.currentTarget.x - enemy.currentCellX;
- var oy = enemy.currentTarget.y - enemy.currentCellY;
+ var laneData = levelData.spawnPoints.find(function (sp) {
+ return sp.id === enemy.spawnerId;
+ });
+ var laneOffset = (enemy.laneIndex - (laneData.lanes - 1) / 2) * laneData.laneOffset;
+ var targetX = enemy.currentTarget.x + laneOffset;
+ var targetY = enemy.currentTarget.y;
+ var ox = targetX - enemy.currentCellX;
+ var oy = targetY - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
- enemy.currentCellX = enemy.currentTarget.x;
- enemy.currentCellY = enemy.currentTarget.y;
- enemy.cellX = Math.round(enemy.currentCellX);
- enemy.cellY = Math.round(enemy.currentCellY);
+ enemy.currentCellX = targetX;
+ enemy.currentCellY = targetY;
+ enemy.cellX = Math.round(enemy.currentTarget.x);
+ enemy.cellY = Math.round(enemy.currentTarget.y);
enemy.currentTarget = undefined;
} else {
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
@@ -1693,9 +1596,9 @@
frameSprite.visible = false;
allFrames.push(frameSprite);
}
var currentFrame = 0;
- var frameDuration = 4;
+ var frameDuration = 5;
var frameTicker = 0;
if (allFrames.length > 0) {
allFrames[currentFrame].visible = true;
}
@@ -2700,9 +2603,19 @@
cell.type = 1;
}
}
}
- grid.pathFind();
+ // --- POCZĄTEK POPRAWKI ---
+ // Przeliczamy na nowo wszystkie zdefiniowane ścieżki
+ if (levelData.spawnPoints) {
+ levelData.spawnPoints.forEach(function (spawner) {
+ var goalCell = grid.getCell(spawner.goalX, spawner.goalY);
+ if (goalCell) {
+ grid.calculatePath(spawner.id, goalCell);
+ }
+ });
+ }
+ // --- KONIEC POPRAWKI ---
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
@@ -2929,56 +2842,60 @@
'guard': {
1: {
name: 'Guard Tower',
cost: 30,
- damage: 4,
+ damage: 5,
fireRate: 60,
- range: 4 * CELL_SIZE,
+ range: 5 * CELL_SIZE,
bulletSpeed: 7,
graphic: 'arrow_guard'
},
2: {
name: 'Veteran Guard',
- cost: 60,
- damage: 8,
+ cost: 80,
+ // Zwiększono z 60
+ damage: 9,
fireRate: 60,
- range: 4 * CELL_SIZE,
+ range: 5 * CELL_SIZE,
bulletSpeed: 7,
graphic: 'arrow_guard'
},
3: {
name: 'Elite Guard',
- cost: 120,
- damage: 16,
+ cost: 155,
+ // Zwiększono ze 120
+ damage: 17,
fireRate: 55,
- range: 4.5 * CELL_SIZE,
+ range: 5.5 * CELL_SIZE,
bulletSpeed: 7,
graphic: 'arrow_guard'
}
},
'crossbow': {
1: {
name: 'Crossbow Tower',
cost: 50,
- damage: 12,
+ damage: 13,
fireRate: 150,
range: 7 * CELL_SIZE,
bulletSpeed: 15,
graphic: 'arrow_crossbow'
},
2: {
name: 'Heavy Crossbow',
- cost: 100,
- damage: 24,
+ cost: 130,
+ // Zwiększono ze 100
+ damage: 25,
fireRate: 150,
range: 7 * CELL_SIZE,
bulletSpeed: 15,
graphic: 'arrow_crossbow'
},
3: {
name: 'Arbalest',
- cost: 200,
- damage: 50,
+ cost: 260,
+ // Zwiększono z 200
+ damage: 51,
fireRate: 150,
range: 7.5 * CELL_SIZE,
bulletSpeed: 15,
graphic: 'arrow_crossbow'
@@ -2987,45 +2904,48 @@
'mage': {
1: {
name: 'Mage Tower',
cost: 40,
- damage: 6,
+ damage: 7,
fireRate: 90,
- range: 4 * CELL_SIZE,
+ range: 5 * CELL_SIZE,
bulletSpeed: 8,
bulletType: 'normal',
graphic: 'bullet'
},
'2F': {
name: 'Fire Mage',
- cost: 80,
- damage: 6,
+ cost: 105,
+ // Zwiększono z 80
+ damage: 7,
fireRate: 120,
- range: 5 * CELL_SIZE,
+ range: 6 * CELL_SIZE,
bulletSpeed: 8,
bulletType: 'splash',
splashDamage: 3,
splashRadius: 1 * CELL_SIZE,
graphic: 'projectile_fireball'
},
'3F': {
name: 'Pyromancer',
- cost: 160,
- damage: 12,
+ cost: 210,
+ // Zwiększono ze 160
+ damage: 13,
fireRate: 120,
- range: 5.5 * CELL_SIZE,
+ range: 6.5 * CELL_SIZE,
bulletSpeed: 8,
bulletType: 'splash',
splashDamage: 8,
splashRadius: 1.5 * CELL_SIZE,
graphic: 'projectile_fireball'
},
'2W': {
name: 'Water Mage',
- cost: 80,
- damage: 4,
+ cost: 105,
+ // Zwiększono z 80
+ damage: 5,
fireRate: 120,
- range: 5 * CELL_SIZE,
+ range: 6 * CELL_SIZE,
bulletSpeed: 8,
bulletType: 'slow',
slowAmount: 0.6,
slowDuration: 120,
@@ -3033,12 +2953,13 @@
graphic: ['ice_spike_1', 'ice_spike_2', 'ice_spike_3', 'ice_spike_4', 'ice_spike_5']
},
'3W': {
name: 'Ice Archmage',
- cost: 160,
- damage: 10,
+ cost: 210,
+ // Zwiększono ze 160
+ damage: 11,
fireRate: 110,
- range: 5.5 * CELL_SIZE,
+ range: 6.5 * CELL_SIZE,
bulletSpeed: 8,
bulletType: 'slow',
slowAmount: 0.5,
slowDuration: 120,
@@ -3046,12 +2967,13 @@
graphic: ['ice_spike_1', 'ice_spike_2', 'ice_spike_3', 'ice_spike_4', 'ice_spike_5']
},
'2L': {
name: 'Lightning Mage',
- cost: 80,
- damage: 7,
+ cost: 105,
+ // Zwiększono z 80
+ damage: 8,
fireRate: 150,
- range: 6 * CELL_SIZE,
+ range: 7 * CELL_SIZE,
bulletSpeed: 20,
bulletType: 'chain',
chainTargets: 2,
chainDamageFalloff: 2,
@@ -3059,12 +2981,13 @@
graphic: 'bullet'
},
'3L': {
name: 'Stormcaller',
- cost: 160,
- damage: 15,
+ cost: 210,
+ // Zwiększono ze 160
+ damage: 16,
fireRate: 150,
- range: 6.5 * CELL_SIZE,
+ range: 7.5 * CELL_SIZE,
bulletSpeed: 20,
bulletType: 'chain',
chainTargets: 3,
chainDamageFalloff: 2,
@@ -3083,17 +3006,19 @@
}
},
'2A': {
name: 'Banner of Fury',
- cost: 100,
+ cost: 130,
+ // Zwiększono ze 100
range: 4 * CELL_SIZE,
aura: {
'attackSpeed': 1.3
}
},
'2B': {
name: 'Banner of Command',
- cost: 100,
+ cost: 130,
+ // Zwiększono ze 100
range: 4 * CELL_SIZE,
aura: {
'attackSpeed': 1.15,
'damage': 1.15
@@ -3222,85 +3147,159 @@
1: {
levelName: "Level 1: Royal Road",
initialGold: 600,
initialLives: 20,
- mapLayout: ["22222222110S011222222222", "22222222AA00011222222222", "222222221100011222222222", "222222221100011222222222", "2222222211000AA222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "22222222110E011222222222"],
+ mapLayout: ["222222221100011222222222", "22222222AA00011222222222", "222222221100011222222222", "222222221100011222222222", "2222222211000AA222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "22222222110E011222222222"],
castleMapLayout: ["11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111", "11111111111111111"],
- waves: function () {
- var waves = [{
- wave: 1,
- composition: [{
+ spawnPoints: [{
+ id: 'mainPath',
+ startX: 12,
+ startY: 5,
+ goalX: 12,
+ goalY: 34,
+ lanes: 2,
+ laneOffset: 1.2
+ }],
+ waves: [{
+ wave: 1,
+ phases: [{
+ delay: 0,
+ interval: 140,
+ units: [{
+ spawner: 'mainPath',
type: 'normal',
- count: 8
+ count: 12
}]
+ }]
+ }, {
+ wave: 2,
+ phases: [{
+ delay: 0,
+ interval: 100,
+ units: [{
+ spawner: 'mainPath',
+ type: 'normal',
+ count: 10
+ }]
}, {
- wave: 2,
- composition: [{
+ delay: 450,
+ interval: 120,
+ units: [{
+ spawner: 'mainPath',
+ type: 'shield',
+ count: 6
+ }]
+ }]
+ }, {
+ wave: 3,
+ phases: [{
+ delay: 0,
+ interval: 110,
+ units: [{
+ spawner: 'mainPath',
type: 'normal',
- count: 12
+ count: 8
}, {
+ spawner: 'mainPath',
type: 'shield',
count: 3
}]
}, {
- wave: 3,
- composition: [{
- type: 'normal',
- count: 8
- }, {
+ delay: 600,
+ interval: 60,
+ units: [{
+ spawner: 'mainPath',
+ type: 'rider',
+ count: 9
+ }]
+ }]
+ }, {
+ wave: 4,
+ phases: [{
+ delay: 0,
+ interval: 130,
+ units: [{
+ spawner: 'mainPath',
type: 'shield',
- count: 7
- }, {
+ count: 10
+ }]
+ }, {
+ delay: 240,
+ interval: 100,
+ units: [{
+ spawner: 'mainPath',
type: 'archer',
- count: 3
+ count: 8
}]
}, {
- wave: 4,
- composition: [{
+ delay: 500,
+ interval: 80,
+ units: [{
+ spawner: 'mainPath',
type: 'rider',
count: 6
- }, {
+ }]
+ }]
+ }, {
+ wave: 5,
+ phases: [{
+ delay: 0,
+ interval: 70,
+ units: [{
+ spawner: 'mainPath',
+ type: 'normal',
+ count: 15
+ }]
+ }, {
+ delay: 400,
+ interval: 110,
+ units: [{
+ spawner: 'mainPath',
type: 'shield',
- count: 6
+ count: 10
}, {
+ spawner: 'mainPath',
type: 'archer',
- count: 4
+ count: 8
}]
}, {
- wave: 5,
- composition: [{
+ delay: 400,
+ interval: 60,
+ units: [{
+ spawner: 'mainPath',
+ type: 'rider',
+ count: 10
+ }]
+ }]
+ }, {
+ wave: 6,
+ phases: [{
+ delay: 0,
+ interval: 100,
+ units: [{
+ spawner: 'mainPath',
type: 'shield',
count: 10
}, {
- type: 'rider',
- count: 8
- }, {
+ spawner: 'mainPath',
type: 'archer',
- count: 6
+ count: 8
}]
}, {
- wave: 6,
- composition: [{
+ delay: 400,
+ interval: 80,
+ units: [{
+ spawner: 'mainPath',
type: 'chieftain',
count: 1,
isBoss: true
}, {
- type: 'shield',
- count: 8
- }, {
- type: 'archer',
- count: 6
- }, {
+ spawner: 'mainPath',
type: 'rider',
- count: 4
+ count: 10
}]
- }];
- for (var i = 0; i < waves.length; i++) {
- waves[i].wave = i + 1;
- }
- return waves;
- }(),
- specialTiles: []
+ }]
+ }]
}
};
var UNIT_DATA = {
'swordsman': {
@@ -3959,11 +3958,9 @@
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
- tower.startConstructionAnimation(5, function () {
- grid.pathFind();
- });
+ tower.startConstructionAnimation(5, function () {});
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
@@ -4086,10 +4083,8 @@
}
}
}
}
- grid.pathFind();
- grid.calculateAggressionPath();
var castleGridWidth = 17;
var castleGridHeight = 22;
castleGrid = new Grid(castleGridWidth, castleGridHeight);
castleGrid.x = (2048 - castleGridWidth * CELL_SIZE) / 2 - 30;
@@ -4431,8 +4426,28 @@
}
initializeGameState(levelData);
setupScene();
setupGrid(levelData);
+ if (levelData.spawnPoints) {
+ levelData.spawnPoints.forEach(function (spawner) {
+ // --- POCZĄTEK POPRAWKI ---
+ // Znajdź wszystkie komórki końcowe na szerokości ścieżki
+ var goalCells = [];
+ for (var i = -1; i <= 1; i++) {
+ // Sprawdzamy środek, lewo i prawo od punktu docelowego
+ var cell = grid.getCell(spawner.goalX + i, spawner.goalY);
+ // Upewniamy się, że komórka istnieje i jest częścią ścieżki (type 0)
+ if (cell && cell.type === 0) {
+ goalCells.push(cell);
+ }
+ }
+ // Jeśli znaleziono jakiekolwiek komórki docelowe, oblicz ścieżkę do nich
+ if (goalCells.length > 0) {
+ grid.calculatePath(spawner.id, goalCells);
+ }
+ // --- KONIEC POPRAWKI ---
+ });
+ }
setupStatsPanel();
setupUIAndPanels();
}
function handlePanelClick(x, y) {
@@ -4775,75 +4790,91 @@
} else if (waveInProgress) {
if (!waveSpawned) {
spawnQueue = [];
waveClock = 0;
+ enemiesKilled = 0;
var waveData = levelData.waves[currentWave - 1];
- if (waveData && waveData.composition) {
- var spawnList = [];
- waveData.composition.forEach(function (group) {
- for (var i = 0; i < group.count; i++) {
- spawnList.push({
- type: group.type,
- isBoss: group.isBoss || false
+ if (waveData && waveData.phases) {
+ var allEnemiesInWave = [];
+ var currentTimeOffset = 0;
+ var globalUnitIndex = 0;
+ waveData.phases.forEach(function (phase) {
+ currentTimeOffset += phase.delay || 0;
+ var timeInThisPhase = 0;
+ phase.units.forEach(function (unitGroup) {
+ var spawner = levelData.spawnPoints.find(function (sp) {
+ return sp.id === unitGroup.spawner;
});
- }
+ if (!spawner || spawner.lanes <= 0) {
+ return;
+ }
+ // --- POCZĄTEK NOWEJ LOGIKI PAROWANIA ---
+ // Pętla iteruje teraz co liczbę rzędów (czyli co parę)
+ for (var i = 0; i < unitGroup.count; i += spawner.lanes) {
+ var pairIndex = i / spawner.lanes;
+ var spawnTimeForPair = currentTimeOffset + pairIndex * (phase.interval || 120);
+ // Ta pętla tworzy jednostki w parze (lub pojedynczo, jeśli 1 rząd)
+ for (var j = 0; j < spawner.lanes; j++) {
+ if (i + j < unitGroup.count) {
+ // Zabezpieczenie dla nieparzystej liczby wrogów
+ spawnQueue.push({
+ unitData: {
+ spawner: unitGroup.spawner,
+ type: unitGroup.type,
+ isBoss: unitGroup.isBoss || false
+ },
+ spawnTime: spawnTimeForPair,
+ // Ten sam czas dla całej pary
+ unitIndex: globalUnitIndex
+ });
+ allEnemiesInWave.push(unitGroup.type);
+ globalUnitIndex++;
+ }
+ }
+ }
+ // --- KONIEC NOWEJ LOGIKI PAROWANIA ---
+ timeInThisPhase += Math.ceil(unitGroup.count / spawner.lanes) * (phase.interval || 120);
+ });
+ currentTimeOffset += timeInThisPhase;
});
- enemiesInCurrentWave = spawnList.length;
+ enemiesInCurrentWave = allEnemiesInWave.length;
updateUI();
- var firstPhaseCount = Math.ceil(enemiesInCurrentWave * 0.4);
- var firstPhaseInterval = 60;
- var pauseDuration = 10 * 60;
- var secondPhaseInterval = 80;
- for (var i = 0; i < spawnList.length; i++) {
- var spawnDelay;
- if (i < firstPhaseCount) {
- spawnDelay = i * firstPhaseInterval;
- } else {
- var firstPhaseTotalTime = (firstPhaseCount > 0 ? firstPhaseCount - 1 : 0) * firstPhaseInterval;
- var secondPhaseStartTime = firstPhaseTotalTime + pauseDuration;
- var indexInSecondPhase = i - firstPhaseCount;
- spawnDelay = secondPhaseStartTime + indexInSecondPhase * secondPhaseInterval;
- }
- spawnQueue.push({
- unitData: spawnList[i],
- spawnTime: spawnDelay,
- unitIndex: i
- });
- }
}
waveSpawned = true;
}
waveClock++;
if (spawnQueue.length > 0 && waveClock >= spawnQueue[0].spawnTime) {
var spawnData = spawnQueue.shift();
- var enemy = new Enemy(spawnData.unitData.type);
- var spawnPoints = grid.spawns;
- var spawnCell = spawnPoints[Math.floor(Math.random() * spawnPoints.length)];
- var lanes = [-0.25, 0, 0.25];
- var laneOffset = lanes[spawnData.unitIndex % lanes.length];
- var spawnY = 5;
- enemy.currentCellX = spawnCell.x + laneOffset;
- enemy.currentCellY = spawnY;
- enemy.cellX = spawnCell.x;
- enemy.cellY = spawnY;
- enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
- enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
- enemy.waveNumber = currentWave;
- if (spawnData.unitData.isBoss) {
- enemy.isBoss = true;
- }
- var healthMultiplier = Math.pow(1.10, currentWave);
- enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
- enemy.health = enemy.maxHealth;
- if (enemy.isFlying) {
- enemyLayerTop.addChild(enemy);
- if (enemy.shadow) {
- enemyLayerMiddle.addChild(enemy.shadow);
+ var spawner = levelData.spawnPoints.find(function (sp) {
+ return sp.id === spawnData.unitData.spawner;
+ });
+ if (spawner) {
+ var enemy = new Enemy(spawnData.unitData.type, spawnData.unitData.spawner);
+ enemy.laneIndex = spawnData.unitIndex % spawner.lanes;
+ var laneOffset = (enemy.laneIndex - (spawner.lanes - 1) / 2) * spawner.laneOffset;
+ enemy.currentCellX = spawner.startX + laneOffset;
+ enemy.currentCellY = spawner.startY;
+ enemy.cellX = spawner.startX;
+ enemy.cellY = spawner.startY;
+ enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
+ enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
+ enemy.waveNumber = currentWave;
+ if (spawnData.unitData.isBoss) {
+ enemy.isBoss = true;
}
- } else {
- enemyLayerBottom.addChild(enemy);
+ var healthMultiplier = Math.pow(1.15, currentWave);
+ enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
+ enemy.health = enemy.maxHealth;
+ if (enemy.isFlying) {
+ enemyLayerTop.addChild(enemy);
+ if (enemy.shadow) {
+ enemyLayerMiddle.addChild(enemy.shadow);
+ }
+ } else {
+ enemyLayerBottom.addChild(enemy);
+ }
+ enemies.push(enemy);
}
- enemies.push(enemy);
}
if (spawnQueue.length === 0 && enemies.length === 0) {
waveInProgress = false;
if (numberOfTaverns > 0) {