/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var AchievementsScreen = Container.expand(function () { var self = Container.call(this); self.visible = false; var ACHIEVEMENT_CONFIG = { 'foundations_defense': { name: "Foundations of Defense", description: "Complete 2 maps (1-6) using only Level 1 towers.", total: 2 }, 'advanced_engineering': { name: "Advanced Engineering", description: "Complete all 6 maps (1-6) using towers up to Level 2.", total: 6 }, 'minimalist_fortress': { name: "Minimalist Fortress", description: "Complete 6 campaign maps building a maximum of 10 towers on each.", total: 6 }, 'lone_defender': { name: "Lone Defender", description: "Complete 6 campaign maps without recruiting any army units.", total: 6 }, 'arcane_abstinence': { name: "Arcane Abstinence", description: "Complete all 6 maps (1-6) without using any spells.", total: 6 }, 'what_does_this_do': { name: "What Does This Do?", description: "Use every type of spell in a single level.", total: 1, isSingle: true }, 'master_architect': { name: "Master Architect", description: "Build all 10 unique building types in your castle in a single game.", total: 1, isSingle: true }, 'grand_builder': { name: "Grand Builder", description: "Build a total of 15 Home buildings.", total: 15, isGlobal: true }, 'royal_treasurer': { name: "Royal Treasurer", description: "Collect a total of 100,000 gold.", total: 100000, isGlobal: true }, 'they_dont_like_it': { name: "They Don't Like It", description: "Click on your citizens 30 times.", total: 30, isGlobal: true } }; self.attachAsset('bg_achievements', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); // DODANA GRAFIKA Z TYTUŁEM self.attachAsset('achievements_title_graphic', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 250 }); var backButton = new Container(); backButton.x = 2048 - 250; backButton.y = 150; var backButtonBg = backButton.attachAsset('button_back_encyclopedia', { anchorX: 0.5, anchorY: 0.5 }); self.addChild(backButton); backButton.down = function () { self.hide(); }; var listContainer = new Container(); listContainer.x = 2048 / 2; listContainer.y = 550; // Przesunięcie listy w dół self.addChild(listContainer); function renderAchievements() { listContainer.removeChildren(); var yPos = 0; var achievementIds = Object.keys(ACHIEVEMENT_CONFIG); for (var i = 0; i < achievementIds.length; i++) { var id = achievementIds[i]; var config = ACHIEVEMENT_CONFIG[id]; var progress = 0; if (progressionData.achievements) { var achData = progressionData.achievements; if (id === 'foundations_defense' && achData.foundationsCompletedMaps) { progress = achData.foundationsCompletedMaps.length; } if (id === 'advanced_engineering' && achData.advancedCompletedMaps) { progress = achData.advancedCompletedMaps.length; } if (id === 'minimalist_fortress' && achData.minimalistCompletedMaps) { progress = achData.minimalistCompletedMaps.length; } if (id === 'lone_defender' && achData.loneDefenderCompletedMaps) { progress = achData.loneDefenderCompletedMaps.length; } if (id === 'arcane_abstinence' && achData.arcaneAbstinenceCompletedMaps) { progress = achData.arcaneAbstinenceCompletedMaps.length; } if (id === 'master_architect' && achData.buildingsConstructed) { progress = Object.keys(achData.buildingsConstructed).length; } if (id === 'grand_builder' && achData.homesBuilt) { progress = achData.homesBuilt; } if (id === 'royal_treasurer' && achData.totalGoldCollected) { progress = achData.totalGoldCollected; } if (id === 'they_dont_like_it' && achData.npcClicks) { progress = achData.npcClicks; } if (id === 'what_does_this_do' && achData.whatDoesThisDoRewarded) { progress = 1; } } progress = Math.min(progress, config.total); var isCompleted = progress >= config.total; var textColor = isCompleted ? 0x00FF00 : 0x000000; var achContainer = new Container(); achContainer.y = yPos; listContainer.addChild(achContainer); var nameText = new Text2(config.name + ": " + config.description, { size: 56, fill: textColor, weight: 'normal', wordWrap: true, wordWrapWidth: 1000 }); nameText.anchor.set(0, 0.5); nameText.x = -800; achContainer.addChild(nameText); var progressText = new Text2(progress + " / " + config.total, { size: 60, fill: textColor, weight: 'bold' }); progressText.anchor.set(1, 0.5); progressText.x = 800; achContainer.addChild(progressText); yPos += 180; } } self.show = function () { self.visible = true; renderAchievements(); }; self.hide = function () { self.visible = false; if (titleScreenContainer) { titleScreenContainer.visible = true; } }; return self; }); 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 menuBackground = self.attachAsset('panel_popup_background', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 2048; menuBackground.height = 500; 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 buildingData = BUILDING_DATA[self.building.id]; if (buildingData) { var nameText = new Text2(buildingData.name, { size: 80, fill: 0xFFD700, weight: 800 }); nameText.anchor.set(0.5, 0.5); nameText.x = 0; nameText.y = -120; self.addChild(nameText); var descriptionText = new Text2(buildingData.description, { size: 60, fill: 0xFFFFFF, weight: 500, align: 'center', wordWrap: true, wordWrapWidth: menuBackground.width - 300 }); descriptionText.anchor.set(0.5, 0); descriptionText.x = 0; descriptionText.y = nameText.y + 130; // Zwiększony odstęp od tytułu self.addChild(descriptionText); } 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.props.graphic === 'projectile_fireball') { self.allFrames[0].tint = 0xFF8C00; } if (self.props.bulletType === 'slow') { self.allFrames.forEach(function (frame) { frame.tint = 0xADD8E6; }); } self.update = function () { if (self.hitTarget) { if (self.props.graphic === 'projectile_fireball' || self.props.graphic === 'projectile_arcane') { self.allFrames.forEach(function (frame) { frame.rotation += 0.12; }); } return; } 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' || self.props.graphic === 'projectile_arcane') { frame.rotation += 0.2; } else { frame.rotation = angle; } }); if (distance < self.props.bulletSpeed) { self.hitTarget = true; var healthBeforeHit = self.targetEnemy.health; self.targetEnemy.takeDamage(self.props.damage); var healthAfterHit = self.targetEnemy.health; if (healthBeforeHit > 0 && healthAfterHit <= 0) { if (self.props.sourceTowerType === 'mage' && progressionData.upgrades['towers_mage_essence_tap_2']) { setMana(mana + 2); } } switch (self.props.bulletType) { case 'slow': spellEffectLayer.addChild(new IceSpikeExplosion(self.x, self.y, 1.5)); if (self.props.canStun && progressionData.upgrades['champions_ranger_stun_1']) { var stunChance = progressionData.upgrades['champions_ranger_stun_1'] * 0.10; if (Math.random() < stunChance) { self.targetEnemy.isStunned = true; self.targetEnemy.stunTimer = 120; break; } } 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': if (self.props.graphic === 'projectile_fireball') { spellEffectLayer.addChild(new Explosion(self.x, self.y, self.props.splashScale)); } enemies.forEach(function (enemy) { if (enemy !== self.targetEnemy) { var distToSplash = Math.sqrt(Math.pow(self.x - enemy.x, 2) + Math.pow(self.y - enemy.y, 2)); if (distToSplash < self.props.splashRadius) { enemy.takeDamage(self.props.splashDamage); if (self.props.canBurn && progressionData.upgrades['champions_archmage_burn_1']) { if (Math.random() < 0.30) { enemy.isBurning = true; enemy.burnDuration = 220; enemy.burnDamage = 6; } } } } }); break; case 'chain': spellEffectLayer.addChild(new LightningStrikeEffect(self.x, self.y)); if (self.props.stunChance && Math.random() < self.props.stunChance) { self.targetEnemy.isStunned = true; self.targetEnemy.stunTimer = self.props.stunDuration || 90; } 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 && distToChain < minChainDist) { minChainDist = distToChain; nextTarget = enemy; } } }); if (nextTarget) { nextTarget.takeDamage(damage); if (self.props.chainStunChance && Math.random() < self.props.chainStunChance) { nextTarget.isStunned = true; nextTarget.stunTimer = self.props.stunDuration || 90; } spellEffectLayer.addChild(new ChainLightningVisual(lastTarget, nextTarget)); chainedTargets.push(nextTarget); lastTarget = nextTarget; } else { break; } } } break; } if (self.props.graphic === 'projectile_arcane') { tween(self, { scaleX: 4.5, scaleY: 4.5 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { LK.setTimeout(function () { tween(self, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); }, 1500); } }); } else { 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 EndScreen = Container.expand(function (isVictory, value1, totalPoints, maxPoints) { var self = Container.call(this); self.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000, alpha: 0.7 }); var panel = new Container(); self.addChild(panel); panel.x = 2048 / 2; panel.y = 2732 / 2; var panelBg = panel.attachAsset('panel_endscreen_background', { anchorX: 0.5, anchorY: 0.5 }); if (currentLevelNumber == 7 && !isVictory) { var wavesSurvived = value1; var highScore = progressionData.hellgatesHighScore || 0; var title = new Text2("You Have Fallen", { size: 100, fill: 0xFF0000, weight: 800 }); title.anchor.set(0.5, 0.5); title.y = -300; panel.addChild(title); var currentScoreText = new Text2("Waves Survived: " + wavesSurvived, { size: 60, fill: 0xFFFFFF, weight: 600 }); currentScoreText.anchor.set(0.5, 0.5); currentScoreText.y = -120; panel.addChild(currentScoreText); var highScoreText = new Text2("Best Score: " + highScore, { size: 60, fill: 0xAAAAAA }); highScoreText.anchor.set(0.5, 0.5); highScoreText.y = 0; panel.addChild(highScoreText); } else { var pointsEarned = value1; var titleText = isVictory ? "Level Complete!" : "You Lose!"; var titleColor = isVictory ? 0xFFD700 : 0xFF0000; var title = new Text2(titleText, { size: 100, fill: titleColor, weight: 800 }); title.anchor.set(0.5, 0.5); title.y = -300; panel.addChild(title); var earnedText = new Text2("Skill Points Earned: " + pointsEarned, { size: 60, fill: 0xFFFFFF, weight: 600, wordWrap: true, wordWrapWidth: 1200 }); earnedText.anchor.set(0.5, 0.5); earnedText.y = -120; panel.addChild(earnedText); var totalText = new Text2("Total for this map: " + totalPoints + " / " + maxPoints, { size: 60, fill: 0xAAAAAA }); totalText.anchor.set(0.5, 0.5); totalText.y = 0; panel.addChild(totalText); } var returnButton = new Container(); returnButton.y = 250; panel.addChild(returnButton); var buttonBg = returnButton.attachAsset('button_return_to_menu', { anchorX: 0.5, anchorY: 0.5 }); returnButton.down = function () { clearActiveWeatherModifiers(); game.removeChildren(); if (LK.gui.top) { LK.gui.top.removeChildren(); } showTitleScreen(); }; return self; }); var Enemy = Container.expand(function (type, spawnerId, laneId) { var self = Container.call(this); self.type = type || 'normal'; self.spawnerId = spawnerId; self.laneId = laneId; self.laneOffset = 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.isPoisoned = false; self.poisonDuration = 0; self.poisonDamage = 0; self.lastPoisonTick = 0; self.poisonFlashTicker = 0; 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.currentTarget = undefined; self.maxHealth = 78; self.health = self.maxHealth; self.waveNumber = currentWave; self.isFlying = 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.detectionRange = 4 * CELL_SIZE; self.targetUnit = null; self.hasBeenResurrected = false; self.goldValue = 10; switch (self.type) { case 'orc_shield': self.maxHealth = 120; self.damage = 3; self.manaCategory = 'strong'; self.goldValue = 30; break; case 'orc_rider': self.maxHealth = 100; self.damage = 7; self.speed *= 1.5; self.manaCategory = 'strong'; self.goldValue = 40; break; case 'orc_chieftain': self.maxHealth = 550; self.damage = 14; self.isBoss = true; self.manaCategory = 'boss'; self.goldValue = 300; break; case 'orc_archer': self.maxHealth = 50; self.damage = 9; self.attackSpeed = 48; self.isRanged = true; self.range = 6 * CELL_SIZE; self.detectionRange = 6.5 * CELL_SIZE; self.manaCategory = 'strong'; self.goldValue = 35; break; case 'undead_soldier': self.maxHealth = 100; self.damage = 5; self.speed *= 0.9; self.manaCategory = 'weak'; self.goldValue = 25; break; case 'undead_archer': self.maxHealth = 70; self.damage = 11; self.attackSpeed = 48; self.isRanged = true; self.range = 6 * CELL_SIZE; self.detectionRange = 6.5 * CELL_SIZE; self.manaCategory = 'strong'; self.goldValue = 35; break; case 'undead_horseman': self.maxHealth = 130; self.damage = 8; self.speed *= 1.5; self.manaCategory = 'strong'; self.goldValue = 60; break; case 'death_knight': self.maxHealth = 410; self.damage = 11; self.speed *= 1.1; self.manaCategory = 'special'; self.goldValue = 150; break; case 'undead_shielder': self.maxHealth = 160; self.damage = 3; self.speed *= 0.9; self.manaCategory = 'strong'; self.goldValue = 50; break; case 'lich': self.maxHealth = 400; self.damage = 4; self.speed *= 0.6; self.isBoss = true; self.manaCategory = 'boss'; self.goldValue = 250; self.resurrectionAura = true; self.auraRange = 8 * CELL_SIZE; self.resurrectionChance = 0.5; self.resurrectedStatModifier = 0.5; self.auraGraphics = self.attachAsset('lich_aura_graphic', { anchorX: 0.5, anchorY: 0.5, width: self.auraRange * 2, height: self.auraRange * 2, alpha: 0.6 }); self.auraGraphics.blendMode = 1; self.addChildAt(self.auraGraphics, 0); break; case 'demon_soldier': self.maxHealth = 40; self.damage = 4; self.manaCategory = 'weak'; self.goldValue = 30; break; case 'demon_archer': self.maxHealth = 30; self.damage = 7; self.attackSpeed = 48; self.isRanged = true; self.range = 6 * CELL_SIZE; self.detectionRange = 6.5 * CELL_SIZE; self.manaCategory = 'strong'; self.goldValue = 20; break; case 'demon_defender': self.maxHealth = 80; self.damage = 2; self.speed *= 0.8; self.manaCategory = 'strong'; self.goldValue = 30; break; case 'demon_rider': self.maxHealth = 50; self.damage = 6; self.speed *= 1.6; self.manaCategory = 'strong'; self.goldValue = 40; break; case 'demon_lord': self.maxHealth = 200; self.damage = 12; self.isBoss = true; self.manaCategory = 'boss'; self.goldValue = 100; break; case 'demon_tamer': self.maxHealth = 300; self.damage = 8; self.isBoss = true; self.manaCategory = 'boss'; self.goldValue = 150; self.hasDoTAura = true; self.auraRange = 5 * CELL_SIZE; self.auraDamage = 4; self.auraTickRate = 60; self.auraTicker = 0; self.auraGraphics = self.attachAsset('aura_demon_tamer', { anchorX: 0.5, anchorY: 0.5, width: self.auraRange * 2, height: self.auraRange * 2, alpha: 0.9 }); self.auraGraphics.blendMode = 1; self.addChildAt(self.auraGraphics, 0); break; default: self.maxHealth = 75; self.damage = 4; self.manaCategory = 'weak'; self.goldValue = 25; break; } self.originalSpeed = self.speed; self.health = self.maxHealth; self.fireProjectile = function () { if (!self.targetUnit || self.targetUnit.health <= 0) { return; } 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); }; var assetId; if (self.type.startsWith('orc_')) { assetId = self.type; } else { assetId = 'unit_' + self.type; } var enemyGraphics = 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 = -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) { self.health -= amount * self.damageTakenMultiplier; 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.detectionRange * self.detectionRange; 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.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 && unitGraphics.isAura) { unitGraphics = self.targetUnit.children[1]; } 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.type === 'lich' && self.auraGraphics) { self.auraGraphics.rotation += 0.02; } else if (self.type === 'demon_tamer' && self.auraGraphics) { self.auraGraphics.rotation += 0.03; } if (self.hasDoTAura) { self.auraTicker++; if (self.auraTicker >= self.auraTickRate) { self.auraTicker = 0; var auraRangeSq = self.auraRange * self.auraRange; for (var i = 0; i < friendlyUnits.length; i++) { var unit = friendlyUnits[i]; var dx = unit.x - self.x; var dy = unit.y - self.y; if (dx * dx + dy * dy < auraRangeSq) { unit.takeDamage(self.auraDamage); createAuraDamageSparks(unit.x, unit.y); } } } } 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.isPoisoned) { if (LK.ticks - self.lastPoisonTick >= 60) { self.takeDamage(self.poisonDamage); self.lastPoisonTick = LK.ticks; } self.poisonFlashTicker++; if (self.poisonFlashTicker >= 60) { self.poisonFlashTicker = 0; } if (self.poisonFlashTicker < 30) { enemyGraphics.tint = 0x9400D3; } else { enemyGraphics.tint = 0x00FF00; } self.poisonDuration--; if (self.poisonDuration <= 0) { self.isPoisoned = false; enemyGraphics.tint = self.originalTint; } } if (self.health <= 0) { self.health = 0; self.healthBar.width = 0; return false; } self.targetUnit = self.findTarget(); var combatSpeedInPixels = self.speed * CELL_SIZE * 1.2; if (self.targetUnit && self.targetUnit.health > 0) { var dx = self.targetUnit.x - self.x; var dy = self.targetUnit.y - self.y; if (Math.sqrt(dx * dx + dy * dy) < self.range) { if (LK.ticks - self.lastAttack >= self.attackSpeed) { self.attack(); self.lastAttack = LK.ticks; } } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * combatSpeedInPixels; self.y += Math.sin(angle) * combatSpeedInPixels; } } else { var spawnerData = levelData.spawnPoints.find(function (sp) { return sp.id === self.spawnerId; }); if (spawnerData) { var goalPixelX = grid.x + spawnerData.goalX * CELL_SIZE + CELL_SIZE / 2; var goalPixelY = grid.y + spawnerData.goalY * CELL_SIZE + CELL_SIZE / 2; var dxToGoal = goalPixelX - self.x; var dyToGoal = goalPixelY - self.y; var goalRadius = CELL_SIZE * 1.5; if (dxToGoal * dxToGoal + dyToGoal * dyToGoal < goalRadius * goalRadius) { return true; } } if (grid.updateEnemy(self)) { return true; } if (self.currentTarget) { var targetPixelX = grid.x + (self.currentTarget.x + self.laneOffset) * CELL_SIZE + CELL_SIZE / 2; var targetPixelY = grid.y + self.currentTarget.y * CELL_SIZE + CELL_SIZE / 2; var dx = targetPixelX - self.x; var dy = targetPixelY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < combatSpeedInPixels) { if (self.currentTarget.type === 4) { return true; } self.x = targetPixelX; self.y = targetPixelY; self.cellX = self.currentTarget.x; self.cellY = self.currentTarget.y; self.currentTarget = null; } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * combatSpeedInPixels; self.y += Math.sin(angle) * combatSpeedInPixels; } } } healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10; return false; }; return self; }); var Explosion = Container.expand(function (x, y, scale) { var self = Container.call(this); self.x = x; self.y = y; self.scale.set(scale || 1); var allFrames = []; for (var i = 1; i <= 9; 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 = 8; 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; 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); } self.destroy(); return; } 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 FireWallVisual = Container.expand(function (x, y, size) { var self = Container.call(this); self.x = x; self.y = y; var frameNames = []; for (var i = 1; i <= 13; i++) { frameNames.push('fire_wall_' + i); } var allFrames = []; for (var i = 0; i < frameNames.length; i++) { var frameSprite = self.attachAsset(frameNames[i], { anchorX: 0.5, anchorY: 0.5, width: size, height: size }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 7; var frameTicker = 0; var lifetime = 460; if (allFrames.length > 0) { allFrames[currentFrame].visible = true; } self.update = function () { lifetime--; if (lifetime <= 0) { 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; } } }; return self; }); var FogWisp = Container.expand(function () { var self = Container.call(this); var lifetime = 240 + Math.random() * 60; // Ok. 4-5 sekund życia var isFadingOut = false; // Losowa pozycja startowa na ekranie self.x = Math.random() * 2048; self.y = Math.random() * 2732; // Losowy kierunek i prędkość var angle = Math.random() * Math.PI * 2; var speed = 0.3 + Math.random() * 0.3; var dx = Math.cos(angle) * speed; var dy = Math.sin(angle) * speed; // Losowa grafika mgły var fogAssetNames = ['fog_1', 'fog_2', 'fog_3', 'fog_4']; var randomAssetName = fogAssetNames[Math.floor(Math.random() * fogAssetNames.length)]; var sprite = self.attachAsset(randomAssetName, { anchorX: 0.5, anchorY: 0.5 }); sprite.scale.set(1.5 + Math.random() * 1.0); // Fade-in na starcie self.alpha = 0; tween(self, { alpha: 0.5 + Math.random() * 0.2 }, { duration: 1000 }); self.update = function () { if (isFadingOut) { return; } // Ruch self.x += dx; self.y += dy; // Cykl życia lifetime--; if (lifetime <= 0) { isFadingOut = true; tween(self, { alpha: 0 }, { duration: 1000, onFinish: function onFinish() { var index = activeFogWisps.indexOf(self); if (index !== -1) { activeFogWisps.splice(index, 1); } self.destroy(); } }); } }; 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.pathId = unitStats.spawnerId; self.health = self.maxHealth; self.lastAttack = 0; self.targetEnemies = []; self.isUndying = false; self.undyingTimer = 0; self.defense = 0; self.cellX = Math.floor((self.x - grid.x) / CELL_SIZE); self.cellY = Math.floor((self.y - grid.y) / CELL_SIZE); self.currentTarget = null; if (parseInt(currentLevelNumber) === 4) { self.pathOffsetX = (Math.random() - 0.5) * (CELL_SIZE * 0.7); self.pathOffsetY = (Math.random() - 0.5) * (CELL_SIZE * 0.7); } else { self.pathOffsetX = 0; self.pathOffsetY = 0; } 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) { if (self.unitType === 'paladin' && progressionData.upgrades['champions_paladin_block_1']) { var blockChance = progressionData.upgrades['champions_paladin_block_1'] * 0.15; if (Math.random() < blockChance) { return; } } if (self.isUndying) { return; } var finalDamage = Math.max(1, amount - self.defense); self.health -= finalDamage; if (self.health <= 0) { if (self.unitType === 'swordsman' && progressionData.upgrades['army_swordsman_ultimate_4'] && !self.isUndying) { self.isUndying = true; self.undyingTimer = 120; self.health = 1; unitGraphics.tint = 0xFF0000; } else { self.health = 0; } } self.healthBar.width = self.health / self.maxHealth * 70; }; self.findTargets = function (enemyList) { var searchList = enemyList || enemies; var targets = []; var maxTargets = 1; if (self.unitType === 'archer' && progressionData.upgrades['army_archer_ultimate_4']) { maxTargets = 2; } var potentialTargets = []; for (var i = 0; i < searchList.length; i++) { var enemy = searchList[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 < self.detectionRange * self.detectionRange) { potentialTargets.push({ enemy: enemy, dist: distanceSq }); } } potentialTargets.sort(function (a, b) { return a.dist - b.dist; }); for (var i = 0; i < Math.min(potentialTargets.length, maxTargets); i++) { targets.push(potentialTargets[i].enemy); } return targets; }; self.fireProjectile = function () { self.targetEnemies.forEach(function (target) { if (!target || target.health <= 0) { return; } var bulletProps = { damage: self.damage, bulletSpeed: 12, graphic: 'arrow_unit_archer' }; var bullet = new Bullet(self.x, self.y, target, bulletProps); gateContainer.addChild(bullet); bullets.push(bullet); }); }; self.attack = function () { self.targetEnemies.forEach(function (target) { if (!target || target.health <= 0) { return; } if (self.isRanged) { return; } if (self.unitType === 'defender' && progressionData.upgrades['army_defender_ultimate_4']) { if (Math.random() < 0.20) { target.isStunned = true; target.stunTimer = 120; } } target.takeDamage(self.damage); var angle = Math.atan2(target.y - self.y, target.x - self.x); var recoilDistance = 5; var originalX = target.x; var originalY = target.y; tween(target, { x: originalX + Math.cos(angle) * recoilDistance, y: originalY + Math.sin(angle) * recoilDistance }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() { if (target) { tween(target, { x: originalX, y: originalY }, { duration: 120, easing: tween.easeIn }); } } }); var enemyGraphics = target.children[0]; if (enemyGraphics && enemyGraphics.isAura) { enemyGraphics = target.children[1]; } if (enemyGraphics) { var originalTint = enemyGraphics.tint; enemyGraphics.tint = 0xFF0000; LK.setTimeout(function () { if (enemyGraphics && !enemyGraphics.destroyed) { enemyGraphics.tint = originalTint; } }, 100); } createHitSparks(target.x, target.y); }); }; self.die = function () { if (self.unitType === 'horseman' && progressionData.upgrades['army_horseman_ultimate_4']) { if (Math.random() < 0.40) { var swordsmanStats = Object.assign({}, UNIT_DATA['swordsman']); var newUnit = new FriendlyUnit(self.x, self.y, swordsmanStats); friendlyUnitLayer.addChild(newUnit); friendlyUnits.push(newUnit); } } peopleRegenQueue.push(LK.ticks); var unitIndex = friendlyUnits.indexOf(self); if (unitIndex !== -1) { friendlyUnits.splice(unitIndex, 1); } self.destroy(); }; self.update = function () { if (self.isUndying) { self.undyingTimer--; if (self.undyingTimer <= 0) { self.health = 0; } } if (self.health <= 0) { self.die(); return; } self.healthBar.width = self.health / self.maxHealth * 70; var enemiesOnMyPath = enemies.filter(function (enemy) { return enemy.spawnerId === self.pathId; }); if (!self.targetEnemies || self.targetEnemies.length === 0 || self.targetEnemies.some(function (t) { return !t || t.health <= 0 || !t.parent; })) { self.targetEnemies = self.findTargets(enemiesOnMyPath); } if (self.targetEnemies && self.targetEnemies.length > 0) { self.currentTarget = null; var primaryTarget = self.targetEnemies[0]; var dx = primaryTarget.x - self.x; var dy = primaryTarget.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.range) { if (LK.ticks - self.lastAttack >= self.attackSpeed) { if (self.isRanged) { self.fireProjectile(); } else { self.attack(); } self.lastAttack = LK.ticks; } } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } } else { if (enemiesOnMyPath.length > 0) { if ([2, 4, 5, 6].includes(parseInt(currentLevelNumber))) { var combatSpeedInPixels = self.speed; if (!self.currentTarget) { var cell = grid.getCell(self.cellX, self.cellY); var friendlyPathId = self.pathId + "_friendly"; if (!cell || !cell.targets[friendlyPathId]) { var neighbors = [grid.getCell(self.cellX + 1, self.cellY), grid.getCell(self.cellX - 1, self.cellY), grid.getCell(self.cellX, self.cellY + 1), grid.getCell(self.cellX, self.cellY - 1)]; for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; if (neighbor && neighbor.targets[friendlyPathId]) { cell = neighbor; break; } } } if (cell && cell.targets[friendlyPathId]) { self.currentTarget = cell.targets[friendlyPathId][0]; } } if (self.currentTarget) { var pathShiftX = 0; var pathShiftY = 0; if (parseInt(currentLevelNumber) === 4) { pathShiftX = 45; pathShiftY = 38; } var targetPixelX = grid.x + self.currentTarget.x * CELL_SIZE + CELL_SIZE / 2 + self.pathOffsetX + pathShiftX; var targetPixelY = grid.y + self.currentTarget.y * CELL_SIZE + CELL_SIZE / 2 + self.pathOffsetY + pathShiftY; var dx = targetPixelX - self.x; var dy = targetPixelY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < combatSpeedInPixels) { self.x = targetPixelX; self.y = targetPixelY; self.cellX = self.currentTarget.x; self.cellY = self.currentTarget.y; self.currentTarget = null; } else { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * combatSpeedInPixels; self.y += Math.sin(angle) * combatSpeedInPixels; } } } else { var moveX = 0; var moveY = 0; if (enemiesOnMyPath.length > 0) { 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; } } } else { self.currentTarget = null; } } if (self.y < grid.y) { self.die(); } }; return self; }); var HeroUnit = FriendlyUnit.expand(function (startX, startY, heroStats) { var self = FriendlyUnit.call(this, startX, startY, heroStats); self.isHero = true; self.auraCheckTimer = 0; self.unitsInAura = []; if (self.unitType === 'archmage') { isArchmageOnField = true; } if (self.unitType === 'paladin') { self.auraGraphics = self.attachAsset('aura_paladin', { anchorX: 0.5, anchorY: 0.5, width: self.detectionRange * 2, height: self.detectionRange * 2, alpha: 0.8 }); self.auraGraphics.isAura = true; self.addChildAt(self.auraGraphics, 0); } var originalDie = self.die; self.die = function () { if (self.unitType === 'archmage') { isArchmageOnField = false; } if (self.unitType === 'paladin') { self.unitsInAura.forEach(function (unit) { if (unit) { unit.defense = 0; } }); } spawnedHeroCount--; originalDie.call(self); }; var originalUpdate = self.update; self.update = function () { originalUpdate.call(self); if (self.unitType === 'paladin') { self.auraCheckTimer++; if (self.auraGraphics) { self.auraGraphics.rotation += 0.005; } if (self.auraCheckTimer >= 60) { self.auraCheckTimer = 0; self.unitsInAura.forEach(function (unit) { if (unit) { unit.defense = 0; } }); self.unitsInAura = []; friendlyUnits.forEach(function (unit) { if (unit !== self && !unit.isHero) { var dx = unit.x - self.x; var dy = unit.y - self.y; if (dx * dx + dy * dy < self.detectionRange * self.detectionRange) { var baseDefense = 5; if (progressionData.upgrades['champions_paladin_aura_power_1']) { baseDefense = Math.round(baseDefense * 1.20); } unit.defense = baseDefense; self.unitsInAura.push(unit); } } }); } } }; var originalFindTargets = self.findTargets; self.findTargets = function (enemyList) { if (self.unitType === 'ranger_lord') { var searchList = enemyList || enemies; var targets = []; var maxTargets = 5; var potentialTargets = []; for (var i = 0; i < searchList.length; i++) { var enemy = searchList[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 < self.detectionRange * self.detectionRange) { potentialTargets.push({ enemy: enemy, dist: distanceSq }); } } potentialTargets.sort(function (a, b) { return a.dist - b.dist; }); for (var i = 0; i < Math.min(potentialTargets.length, maxTargets); i++) { targets.push(potentialTargets[i].enemy); } return targets; } else { return originalFindTargets.call(self, enemyList); } }; var originalFireProjectile = self.fireProjectile; self.fireProjectile = function () { if (self.unitType === 'ranger_lord') { self.targetEnemies.forEach(function (target) { if (!target || target.health <= 0) { return; } var bulletProps = { damage: self.damage, bulletSpeed: 15, graphic: 'arrow_unit_archer', bulletType: 'slow', slowAmount: 0.7, slowDuration: 120, slowChance: 1.0, canStun: false }; if (progressionData.upgrades['champions_ranger_crit_1']) { if (Math.random() < 0.30) { bulletProps.damage *= 2; } } if (progressionData.upgrades['champions_ranger_stun_1']) { bulletProps.canStun = true; } var bullet = new Bullet(self.x, self.y, target, bulletProps); gateContainer.addChild(bullet); bullets.push(bullet); }); } else if (self.unitType === 'archmage') { self.targetEnemies.forEach(function (target) { if (!target || target.health <= 0) { return; } var fireBullet = function fireBullet(targetEnemy) { var bulletProps = { damage: self.damage, bulletSpeed: 10, graphic: 'projectile_arcane', bulletType: 'splash', splashDamage: self.damage * 0.8, splashRadius: 2.5 * CELL_SIZE, splashScale: 1.5, canBurn: false }; if (progressionData.upgrades['champions_archmage_burn_1']) { bulletProps.canBurn = true; } var bullet = new Bullet(self.x, self.y, targetEnemy, bulletProps); spellEffectLayer.addChild(bullet); bullets.push(bullet); }; fireBullet(target); if (progressionData.upgrades['champions_archmage_multicast_1']) { var multicastChance = progressionData.upgrades['champions_archmage_multicast_1'] * 0.10; if (Math.random() < multicastChance) { LK.setTimeout(function () { if (target && target.parent && target.health > 0) { fireBullet(target); } }, 300); } } }); } else { originalFireProjectile.call(self); } }; 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; if (progressionData.upgrades['magic_essence_3']) { var essenceBonus = 1 + progressionData.upgrades['magic_essence_3'] * 0.05; self.maxHealth = Math.round(self.maxHealth * essenceBonus); } self.health = self.maxHealth; self.baseDamage = 11; // Zmieniono z damage na baseDamage self.damage = self.baseDamage; 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; } // NOWA LOGIKA: Agonia Golema if (progressionData.upgrades['magic_golem_4']) { var missingHpPercent = (self.maxHealth - self.health) / self.maxHealth; var damageBonus = Math.floor(missingHpPercent * 10); // +1 DMG za każde 10% brakującego HP self.damage = self.baseDamage + damageBonus; } 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: {}, 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(); toProcess[0].pathId = pathCounter; toProcess[0].score[pathId] = 0; function processNode(node, targetValue, targetNode) { if (node && node.type === 0) { 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; } } } } while (toProcess.length) { var node = toProcess.shift(); var targetScore = node.score[pathId] + 1; 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.type === 4) { return true; } if (!enemy.currentTarget) { var lanePathId = enemy.spawnerId + "_" + enemy.laneId; if (cell && cell.targets[lanePathId]) { enemy.currentTarget = cell.targets[lanePathId][0]; } } return false; }; }); var GuideScreen = Container.expand(function () { var self = Container.call(this); self.visible = false; self.currentPageIndex = 0; var clickBlocker = self.attachAsset('cell', { width: 2048, height: 2732, alpha: 0 }); self.addChildAt(clickBlocker, 0); clickBlocker.interactive = true; var background = self.attachAsset('level_select_parchment', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2 }); var contentContainer = new Container(); self.addChild(contentContainer); var titleText = new Text2("", { size: 90, fill: 0x332211, weight: 700, align: 'center', wordWrap: true, wordWrapWidth: 1600 }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 350; contentContainer.addChild(titleText); var pageImage = new Container(); pageImage.x = 2048 / 2; pageImage.y = 1000; contentContainer.addChild(pageImage); var descriptionText = new Text2("", { size: 60, fill: 0x443322, weight: 500, align: 'center', wordWrap: true, wordWrapWidth: 1200 }); descriptionText.anchor.set(0.5, 0.5); descriptionText.x = 2048 / 2; descriptionText.y = 1660; contentContainer.addChild(descriptionText); var pageIndicator = new Text2("", { size: 50, fill: 0x332211, weight: 600 }); pageIndicator.anchor.set(0.5, 0.5); pageIndicator.x = 2048 / 2; pageIndicator.y = 2732 - 400; self.addChild(pageIndicator); function renderPage(index) { var pageData = GUIDE_DATA[index]; if (!pageData) { return; } titleText.setText(pageData.title); descriptionText.setText(pageData.text); pageImage.removeChildren(); if (pageData.image) { pageImage.attachAsset(pageData.image, { anchorX: 0.5, anchorY: 0.5 }); } pageIndicator.setText(index + 1 + " / " + GUIDE_DATA.length); } self.show = function () { self.visible = true; self.currentPageIndex = 0; renderPage(self.currentPageIndex); }; self.hide = function () { self.visible = false; }; clickBlocker.down = function () { if (self.currentPageIndex < GUIDE_DATA.length - 1) { self.currentPageIndex++; renderPage(self.currentPageIndex); } else { self.hide(); } }; return self; }); 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 HellgateInfoScreen = Container.expand(function () { var self = Container.call(this); self.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000, alpha: 0.8 }); var panel = new Container(); self.addChild(panel); panel.x = 2048 / 2; panel.y = 2732 / 2; var panelBg = panel.attachAsset('panel_hellgate_info', { anchorX: 0.5, anchorY: 0.5 }); var fullDescription = "You do not earn Skill Points in this mode.\n" + "Here, you test your build against endless waves of enemies.\n" + "With each wave, enemies become stronger\n" + "Your high score will be saved!"; // Cień tekstu var descriptionText_shadow = new Text2(fullDescription, { size: 66, fill: 0xd9d9d9, weight: 600, align: 'center', wordWrap: true, wordWrapWidth: 1150 }); descriptionText_shadow.anchor.set(0.5, 0.5); descriptionText_shadow.x = 5; // Przesunięcie cienia w prawo descriptionText_shadow.y = 250 + 5; // Przesunięcie cienia w dół panel.addChild(descriptionText_shadow); // Główny tekst var descriptionText_main = new Text2(fullDescription, { size: 66, fill: 0xd90f0f, weight: 600, align: 'center', wordWrap: true, wordWrapWidth: 1150 }); descriptionText_main.anchor.set(0.5, 0.5); descriptionText_main.y = 250; panel.addChild(descriptionText_main); var startButton = new Container(); startButton.y = 750; startButton.x = -300; panel.addChild(startButton); var startButtonBg = startButton.attachAsset('button_hellgate_start', { anchorX: 0.5, anchorY: 0.5 }); startButton.down = function () { self.destroy(); startGame('7'); }; var backButton = new Container(); backButton.y = 750; backButton.x = 300; panel.addChild(backButton); var backButtonBg = backButton.attachAsset('button_hellgate_back', { anchorX: 0.5, anchorY: 0.5 }); backButton.down = function () { self.destroy(); }; return self; }); var HeroCard = Container.expand(function (heroId, onSelect) { var self = Container.call(this); var heroData = HERO_DATA[heroId]; self.interactive = true; var bg = self.attachAsset('ui_hero_card', { anchorX: 0.5, anchorY: 0.5 }); var heroGraphic = self.attachAsset(heroData.image, { anchorX: 0.5, anchorY: 0.5 }); heroGraphic.y = -200; heroGraphic.width = 250; heroGraphic.height = 250; var name = new Text2(heroData.name, { size: 60, fill: 0xFFD700, weight: 700 }); name.anchor.set(0.5, 0.5); name.y = -50; self.addChild(name); var statsText = new Text2("HP: " + heroData.maxHealth + " | DMG: " + heroData.damage, { size: 46, fill: 0xFFFFFF, weight: 500 }); statsText.anchor.set(0.5, 0.5); statsText.y = 20; self.addChild(statsText); var specialTitle = new Text2("Special Ability:", { size: 45, fill: 0xd7d412, weight: 600 }); specialTitle.anchor.set(0.5, 0.5); specialTitle.y = 100; self.addChild(specialTitle); var specialDesc = new Text2(heroData.special, { size: 49, fill: 0xFFFFFF, weight: 460, align: 'center', wordWrap: true, wordWrapWidth: bg.width - 125 }); specialDesc.anchor.set(0.5, 0); specialDesc.y = 140; self.addChild(specialDesc); self.down = function () { onSelect(heroId); }; return self; }); var HeroSelectionScreen = Container.expand(function (onComplete) { var self = Container.call(this); self.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000, alpha: 0.8 }); var title = new Text2("Choose Your Champion", { size: 100, fill: 0xFFD700, weight: 800 }); title.anchor.set(0.5, 0.5); title.x = 2048 / 2; title.y = 400; self.addChild(title); var heroIds = Object.keys(HERO_DATA); var cardSpacing = 650; var totalWidth = (heroIds.length - 1) * cardSpacing; var startX = 2048 / 2 - totalWidth / 2; var yPos = 2732 / 2; for (var i = 0; i < heroIds.length; i++) { var heroId = heroIds[i]; var card = new HeroCard(heroId, function (selectedId) { chosenHeroIdForGame = selectedId; if (onComplete) { onComplete(selectedId); } self.destroy(); }); card.x = startX + i * cardSpacing; card.y = yPos; self.addChild(card); } return self; }); var IceSpikeExplosion = Container.expand(function (x, y, scale) { var self = Container.call(this); self.x = x; self.y = y; self.scale.set(scale || 1); var allFrames = []; for (var i = 1; i <= 9; i++) { var frameSprite = self.attachAsset('ice_spike_' + 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 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, width: 250, height: 400 }); frameSprite.blendMode = 1; frameSprite.visible = false; allFrames.push(frameSprite); } var currentFrame = 0; var frameDuration = 4; 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 NPC = Container.expand(function (spawnX, spawnY, graphicAsset) { var self = Container.call(this); self.x = spawnX; self.y = spawnY; self.speed = 0.5 + Math.random() * 0.5; self.state = 'IDLE'; self.stateTimer = Math.random() * 90 + 30; self.path = []; self.currentTargetPos = null; self.interactionPartner = null; self.clickCount = 0; var npcGraphics = self.attachAsset(graphicAsset, { anchorX: 0.5, anchorY: 0.9 }); npcGraphics.scale.set(0.6); self.down = function () { self.clickCount++; if (!progressionData.achievements) { progressionData.achievements = {}; } if (!progressionData.achievements.npcClicks) { progressionData.achievements.npcClicks = 0; } progressionData.achievements.npcClicks++; var message = ""; if (self.clickCount === 1) { message = "Don't click me!"; } else if (self.clickCount === 2) { message = "I'M SERIOUS, STOP!"; } else if (self.clickCount === 3) { message = "THAT'S IT, I'M LEAVING!"; self.state = 'LEAVING'; self.stateTimer = 180; npcGraphics.destroy(); npcGraphics = self.attachAsset('npc_suitcase', { anchorX: 0.5, anchorY: 0.9 }); npcGraphics.scale.set(0.6); } var bubble = new SpeechBubble(message, self.x, self.y - npcGraphics.height); npcLayer.addChild(bubble); }; self.findPath = function (targetCell) { var startCell = castleGrid.getCell(Math.floor((self.x - castleGrid.x) / CELL_SIZE), Math.floor((self.y - castleGrid.y) / CELL_SIZE)); if (!startCell || !targetCell || startCell === targetCell) { return []; } var queue = [[startCell]]; var visited = {}; // ZMIANA: Zamiast new Set() var startKey = startCell.x + ',' + startCell.y; visited[startKey] = true; while (queue.length > 0) { var path = queue.shift(); var lastCell = path[path.length - 1]; if (lastCell === targetCell) { return path; } var neighbors = [lastCell.up, lastCell.down, lastCell.left, lastCell.right].filter(function (n) { if (!n || n.type !== 1) { return false; } var key = n.x + ',' + n.y; return !visited[key]; // ZMIANA: Sprawdzanie w obiekcie }); for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; var key = neighbor.x + ',' + neighbor.y; visited[key] = true; // ZMIANA: Dodawanie do obiektu var newPath = path.slice(); newPath.push(neighbor); queue.push(newPath); } } return []; }; self.findWanderTarget = function () { var validCells = []; for (var i = 0; i < castleGrid.cells.length; i++) { for (var j = 0; j < castleGrid.cells[i].length; j++) { if (castleGrid.cells[i][j].type === 1) { validCells.push(castleGrid.cells[i][j]); } } } if (validCells.length > 0) { var targetCell = validCells[Math.floor(Math.random() * validCells.length)]; self.path = self.findPath(targetCell); if (self.path.length > 0) { self.state = 'WANDERING'; } } }; self.findInteractionPartner = function () { var potentialPartners = npcs.filter(function (p) { return p !== self && !p.interactionPartner; }); var closestPartner = null; var minDistanceSq = Math.pow(6 * CELL_SIZE, 2); potentialPartners.forEach(function (p) { var dx = p.x - self.x; var dy = p.y - self.y; var distSq = dx * dx + dy * dy; if (distSq < minDistanceSq) { minDistanceSq = distSq; closestPartner = p; } }); if (closestPartner) { self.interactionPartner = closestPartner; closestPartner.interactionPartner = self; var partnerCell = castleGrid.getCell(Math.floor((closestPartner.x - castleGrid.x) / CELL_SIZE), Math.floor((closestPartner.y - castleGrid.y) / CELL_SIZE)); self.path = self.findPath(partnerCell); if (self.path.length > 0) { self.state = 'WANDERING'; closestPartner.state = 'INTERRUPTED'; } else { self.interactionPartner.interactionPartner = null; self.interactionPartner = null; self.findWanderTarget(); } } else { self.findWanderTarget(); } }; self.startInteracting = function () { self.state = 'INTERACTING'; self.stateTimer = Math.random() * 240 + 120; if (self.interactionPartner) { self.interactionPartner.state = 'INTERACTING'; self.interactionPartner.stateTimer = self.stateTimer; var bubbleX = (self.x + self.interactionPartner.x) / 2; var bubbleY = Math.min(self.y, self.interactionPartner.y) - 80; var bubble = new SpeechBubble("...", bubbleX, bubbleY, true); npcLayer.addChild(bubble); } }; self.update = function () { switch (self.state) { case 'IDLE': self.stateTimer--; if (self.stateTimer <= 0) { if (Math.random() < 0.5) { self.findInteractionPartner(); } else { self.findWanderTarget(); } } break; case 'WANDERING': if (self.path.length === 0 && !self.currentTargetPos) { if (self.interactionPartner) { self.startInteracting(); } else { self.state = 'IDLE'; self.stateTimer = Math.random() * 90 + 60; } return; } if (!self.currentTargetPos && self.path.length > 0) { var nextCell = self.path.shift(); self.currentTargetPos = { x: castleGrid.x + nextCell.x * CELL_SIZE + CELL_SIZE / 2, y: castleGrid.y + nextCell.y * CELL_SIZE + CELL_SIZE / 2 }; } if (self.currentTargetPos) { var dx = self.currentTargetPos.x - self.x; var dy = self.currentTargetPos.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < self.speed) { self.x = self.currentTargetPos.x; self.y = self.currentTargetPos.y; self.currentTargetPos = null; } else { self.x += dx / dist * self.speed; self.y += dy / dist * self.speed; } } break; case 'INTERACTING': self.stateTimer--; if (self.stateTimer <= 0) { self.state = 'IDLE'; self.stateTimer = Math.random() * 90 + 30; if (self.interactionPartner) { self.interactionPartner.interactionPartner = null; self.interactionPartner.state = 'IDLE'; self.interactionPartner.stateTimer = Math.random() * 90 + 30; self.interactionPartner = null; } } break; case 'LEAVING': self.y += self.speed * 2; self.stateTimer--; if (self.stateTimer <= 0) { tween(self, { alpha: 0 }, { duration: 1000, onFinish: function onFinish() { people--; updateUI(); self.destroy(); } }); self.state = 'GONE'; } break; case 'INTERRUPTED': case 'GONE': break; } }; return self; }); var Notification = Container.expand(function (message) { var self = Container.call(this); var notificationGraphics = self.attachAsset('panel_popup_background', { anchorX: 0.5, anchorY: 0.5 }); // Zakładamy, że panel notyfikacji ma stały, mniejszy rozmiar notificationGraphics.width = 800; notificationGraphics.height = 150; var notificationText = new Text2(message, { size: 50, fill: 0x000000, weight: 800 }); notificationText.anchor.set(0.5, 0.5); 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); var previewGraphics = previewHolder.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); var textStyle = { size: 60, weight: 900, align: 'center' }; var bonusTextShadow = new Text2("", Object.assign({}, textStyle, { fill: 0x000000 })); bonusTextShadow.anchor.set(0.5, 0.5); bonusTextShadow.x = 3; bonusTextShadow.y = 3; previewHolder.addChild(bonusTextShadow); var bonusText = new Text2("", Object.assign({}, textStyle, { fill: 0xFFFFFF })); bonusText.anchor.set(0.5, 0.5); previewHolder.addChild(bonusText); self.updateAppearance = function (specialEffect) { previewGraphics.width = CELL_SIZE * self.itemSize.w; previewGraphics.height = CELL_SIZE * self.itemSize.h; previewGraphics.alpha = 0.6; bonusText.setText(""); bonusTextShadow.setText(""); previewGraphics.tint = 0x00BFFF; if (specialEffect && self.canPlace && self.hasEnoughGold) { var textToShow = ""; if (specialEffect === 'amplifier') { previewGraphics.tint = 0xFFD700; textToShow = "+15% DMG"; } else if (specialEffect === 'catalyst') { previewGraphics.tint = 0x9370DB; textToShow = "+30% ATS"; } bonusText.setText(textToShow); bonusTextShadow.setText(textToShow); } 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; } } if (!self.canPlace || !self.hasEnoughGold) { previewGraphics.tint = 0xFF0000; bonusText.setText(""); bonusTextShadow.setText(""); } }; self.updatePlacementStatus = function (targetGrid) { var validGridPlacement = true; var currentGrid = targetGrid || grid; var currentSpecialEffect = null; 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; } if (cell && cell.specialEffect) { currentSpecialEffect = cell.specialEffect; } } } } 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(currentSpecialEffect); }; self.snapToGrid = function (x, y, targetGrid) { var currentGrid = targetGrid || grid; // Tutaj kluczowa zmiana: obliczamy gridX i gridY tak, aby wskazywały na LEWY GÓRNY róg obiektu na siatce, // ale wizualnie podgląd będzie CENTROWANY na podanym x, y. // x, y, które tu otrzymujemy, to POZYCJA KURSA DZIĘKI game.move. // Jeśli previewGraphics ma anchor 0.5,0.5, to jego pozycja (self.x, self.y) // jest jego wizualnym środkiem. // Aby wyliczyć lewy górny róg siatki, na której stoi obiekt, musimy odjąć połowę rozmiaru obiektu. self.gridX = Math.floor((x - currentGrid.x - self.itemSize.w * CELL_SIZE / 2) / CELL_SIZE); self.gridY = Math.floor((y - currentGrid.y - self.itemSize.h * CELL_SIZE / 2) / CELL_SIZE); // Wizualna pozycja podglądu jest CENTROWANA na przekazanym x, y self.x = x; self.y = y; self.updatePlacementStatus(currentGrid); }; return self; }); var Raindrop = Container.expand(function () { var self = Container.call(this); var dropGraphics = self.attachAsset('raindrop', { anchorX: 0.5, anchorY: 0.5 }); dropGraphics.tint = 0xADD8E6; dropGraphics.scale.set(0.3 + Math.random() * 0.2); self.x = Math.random() * 2048; self.y = -100; var duration = 800 + Math.random() * 600; tween(self, { y: 2732 + 100 }, { duration: duration, easing: tween.linear, onFinish: function onFinish() { self.destroy(); } }); 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 = 160; baseGraphics.height = 170; 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 SourceHero = Container.expand(function (heroId) { var self = Container.call(this); self.heroId = heroId; self.interactive = true; var heroData = HERO_DATA[self.heroId]; var assetId = heroData.image; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.width = 160; baseGraphics.height = 180; var nameLabel = new Text2(heroData.name, { size: 40, fill: 0xFFD700, weight: 800, align: 'center' }); nameLabel.anchor.set(0.5, 0.5); nameLabel.y = -20; self.addChild(nameLabel); var costLabel = new Text2(heroData.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 maxHeroes = 1; if (self.heroId === 'paladin' && progressionData.upgrades['champions_paladin_max_1']) { maxHeroes += progressionData.upgrades['champions_paladin_max_1']; } else if (self.heroId === 'archmage' && progressionData.upgrades['champions_archmage_max_1']) { maxHeroes += progressionData.upgrades['champions_archmage_max_1']; } else if (self.heroId === 'ranger_lord' && progressionData.upgrades['champions_ranger_max_1']) { maxHeroes += progressionData.upgrades['champions_ranger_max_1']; } var finalCost = heroData.cost; if (progressionData.upgrades['champions_cost_1']) { finalCost = Math.floor(finalCost * (1 - progressionData.upgrades['champions_cost_1'] * 0.10)); } costLabel.setText(finalCost); var canAfford = gold >= finalCost && people > 0 && spawnedHeroCount < maxHeroes; self.alpha = canAfford ? 1 : 0.5; }; self.down = function () { var maxHeroes = 1; var talentId = 'champions_' + self.heroId.split('_')[0] + '_max_1'; if (progressionData.upgrades[talentId]) { maxHeroes += progressionData.upgrades[talentId]; } if (spawnedHeroCount >= maxHeroes) { var notification = game.addChild(new Notification("Maximum number of heroes reached!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } var finalCost = heroData.cost; if (progressionData.upgrades['champions_cost_1']) { finalCost = Math.floor(finalCost * (1 - progressionData.upgrades['champions_cost_1'] * 0.10)); } if (gold >= finalCost && people > 0) { levelProgressFlags.wasArmyRecruited = true; setGold(gold - finalCost); people--; spawnedHeroCount++; updateUI(); var spawnPath = levelData.spawnPoints.find(function (p) { return p.id === selectedSpawnPathId; }) || levelData.spawnPoints[0]; var finalStats = Object.assign({}, heroData); if (progressionData.upgrades['champions_hp_1']) { finalStats.maxHealth = Math.round(finalStats.maxHealth * (1 + progressionData.upgrades['champions_hp_1'] * 0.05)); } if (progressionData.upgrades['champions_dmg_1']) { finalStats.damage = Math.round(finalStats.damage * (1 + progressionData.upgrades['champions_dmg_1'] * 0.05)); } finalStats.spawnerId = spawnPath.id; var spawnOffset = spawnPath.friendlySpawnOffset || 0; var spawnX = grid.x + (spawnPath.goalX + spawnOffset) * CELL_SIZE + CELL_SIZE / 2; var spawnY = grid.y + spawnPath.goalY * CELL_SIZE + CELL_SIZE / 2; var newHero = new HeroUnit(spawnX, spawnY, finalStats); friendlyUnitLayer.addChild(newHero); friendlyUnits.push(newHero); } else { var message = gold < finalCost ? "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 SourceSpell = Container.expand(function (spellId) { var self = Container.call(this); self.spellId = spellId; var spellData = SPELL_DATA[self.spellId]; var assetId = 'icon_spell_' + self.spellId; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, width: 198, height: 198 }); 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; var assetId = 'tower_' + self.towerType; var baseGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); baseGraphics.isTowerBase = true; baseGraphics.width = 160; baseGraphics.height = 180; var stats = TOWER_DATA[self.towerType][1]; var towerName = stats.name; // GŁÓWNY TEKST NAZWY WIEŻY - BEZ CIENIOWANIA, POGRUBNIENIA, TYLKO CZYSTY TEKST var typeLabel = new Text2(towerName, { size: 54, fill: 0xFFFFFF, weight: 'normal', stroke: 6, strokeThickness: 5 // Upewniamy się, że grubość obrysu to 0 }); typeLabel.anchor.set(0.5, 0.5); typeLabel.y = -20; self.addChild(typeLabel); // TEKST KOSZTU (pozostaje bez zmian, zgodnie z życzeniem) var initialCost = stats.cost; var costLabel = new Text2(initialCost, { size: 58, fill: 0xFFD700, weight: 'normal', stroke: 6, strokeThickness: 5 // Upewniamy się, że grubość obrysu to 0 }); costLabel.anchor.set(0.5, 0.5); costLabel.y = 20 + 12; self.addChild(costLabel); self.update = function () { 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); } 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 = 170; baseGraphics.height = 190; 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 (activeWeatherEvent === 'STORM') { var notification = game.addChild(new Notification("Recruitment disabled during a storm!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return; } if (LK.ticks - lastRecruitActionTick < 10) { levelProgressFlags.wasArmyRecruited = true; return; } var finalStats = Object.assign({}, unitData); if (progressionData.upgrades['army_cost_1']) { finalStats.cost = Math.floor(finalStats.cost * (1 - progressionData.upgrades['army_cost_1'] * 0.02)); } if (self.unitId === 'swordsman' && progressionData.upgrades['army_swordsman_hp_2']) { finalStats.maxHealth = Math.round(finalStats.maxHealth * (1 + progressionData.upgrades['army_swordsman_hp_2'] * 0.05)); } if (self.unitId === 'horseman' && progressionData.upgrades['army_horseman_stats_2']) { var bonus = 1 + progressionData.upgrades['army_horseman_stats_2'] * 0.03; finalStats.maxHealth = Math.round(finalStats.maxHealth * bonus); finalStats.damage = Math.round(finalStats.damage * bonus); } if (self.unitId === 'defender' && progressionData.upgrades['army_defender_hp_3']) { finalStats.maxHealth = Math.round(finalStats.maxHealth * (1 + progressionData.upgrades['army_defender_hp_3'] * 0.05)); } if (self.unitId === 'archer' && progressionData.upgrades['army_archer_dmg_3']) { finalStats.damage = Math.round(finalStats.damage * (1 + progressionData.upgrades['army_archer_dmg_3'] * 0.05)); } if (self.unitId === 'swordsman' && progressionData.upgrades['army_swordsman_ultimate_4']) { finalStats.damage = Math.round(finalStats.damage * 1.30); } if (gold >= finalStats.cost && people > 0) { lastRecruitActionTick = LK.ticks; var freeUnitChance = (progressionData.upgrades['army_free_unit_2'] || 0) * 0.05; var freeUnitSpawned = Math.random() < freeUnitChance; setGold(gold - finalStats.cost); people--; updateUI(); panelActionTaken = true; var infraBonus = 1 + (progressionData.upgrades['building_infrastructure_2'] || 0) * 0.05; var trainingGroundsCount = castleBuildings.filter(function (b) { return b.id === 'training_ground'; }).length; if (trainingGroundsCount > 0) { var unitBonus = Math.pow(1.10, trainingGroundsCount) * infraBonus; finalStats.maxHealth = Math.round(finalStats.maxHealth * unitBonus); finalStats.damage = Math.round(finalStats.damage * unitBonus); } var hasSmithy = castleBuildings.some(function (b) { return b.id === 'smithy'; }); if (hasSmithy) { var smithyBonus = 1.15 * infraBonus; finalStats.maxHealth = Math.floor(finalStats.maxHealth * smithyBonus); finalStats.damage = Math.floor(finalStats.damage * smithyBonus); } var unitsToSpawn = freeUnitSpawned ? 2 : 1; for (var i = 0; i < unitsToSpawn; i++) { var currentLevelData = LEVEL_DATA_ALL[currentLevelNumber]; var availablePaths = currentLevelData.spawnPoints; var chosenPath; if (availablePaths.length > 1) { chosenPath = availablePaths.find(function (p) { return p.id === selectedSpawnPathId; }); } else { chosenPath = availablePaths[0]; } if (!chosenPath) { chosenPath = availablePaths[0]; } finalStats.spawnerId = chosenPath.id; var randomSpread = CELL_SIZE * 0.8; var spawnOffset = chosenPath.friendlySpawnOffset || 0; var spawnX = grid.x + (chosenPath.goalX + spawnOffset) * CELL_SIZE + CELL_SIZE / 2 + (Math.random() * randomSpread - randomSpread / 2); var spawnY = grid.y + chosenPath.goalY * CELL_SIZE + CELL_SIZE / 2; var newUnit = new FriendlyUnit(spawnX, spawnY, finalStats); friendlyUnitLayer.addChild(newUnit); friendlyUnits.push(newUnit); } } else { var message = gold < finalStats.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 SpeechBubble = Container.expand(function (text, x, y, isGroup) { var self = Container.call(this); self.x = x; self.y = y; var dialogues = ["The harvest should be good this year.", "A fine day for the kingdom.", "Did you know our king has one purple and one blue eye? I've heard he loves music.", "I hear that in MemeTower, a Cursed Crystal holds Demon Pepe captive.", "I used to be an adventurer like you, then I took an arrow in the knee.", "Stay a while and listen.", "Be wary of the Orc Chieftains, they are tougher than they look.", "I heard the creators of this game also made Roll Souls and Walkman Fighters."]; var displayText = isGroup ? dialogues[Math.floor(Math.random() * dialogues.length)] : text; var bubbleGraphics = self.attachAsset('dialogue_bubble', { anchorX: 0.5, anchorY: 1, tint: 0xEFEFEF }); var bubbleText = new Text2(displayText, { size: 55, fill: 0x000000, align: 'center', wordWrap: true, wordWrapWidth: 500 }); bubbleText.anchor.set(0.5, 0.5); self.addChild(bubbleText); var padding = 140; bubbleGraphics.width = bubbleText.width + padding; bubbleGraphics.height = bubbleText.height + padding; bubbleText.y = -bubbleGraphics.height / 2 - 15; self.alpha = 0; tween(self, { alpha: 1, y: y - 10 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { LK.setTimeout(function () { tween(self, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { self.destroy(); } }); }, isGroup ? 5500 : 3500); } }); 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 TalentTreeScreen = Container.expand(function () { var self = Container.call(this); self.visible = false; var currentCategory = null; var skillNodes = {}; var categoryTabs = []; var backgrounds = { 'Towers': self.attachAsset('bg_towers', { visible: false, x: -100 }), 'Army': self.attachAsset('bg_army', { visible: false, x: -100 }), 'Building': self.attachAsset('bg_building', { visible: false, x: -100 }), 'Magic': self.attachAsset('bg_magic', { visible: false, x: -100 }), 'Champions': self.attachAsset('bg_champions', { visible: false, x: -100 }) }; var nodeContainer = new Container(); self.addChild(nodeContainer); var pointsText = new Text2('Points: 0', { size: 70, fill: 0xFFD700, weight: 800 }); pointsText.anchor.set(0.5, 0.5); pointsText.x = 2048 / 2; pointsText.y = 250; self.addChild(pointsText); var backButton = new Container(); backButton.x = 2048 - 180; backButton.y = 450; var backButtonBg = backButton.attachAsset('button_skill_back', { anchorX: 0.5, anchorY: 0.5 }); self.addChild(backButton); backButton.down = function () { self.hide(); }; var resetButton = new Container(); resetButton.x = 2048 - 180; resetButton.y = 660; var resetButtonBg = resetButton.attachAsset('button_skill_reset', { anchorX: 0.5, anchorY: 0.5 }); self.addChild(resetButton); resetButton.down = function () { var spentPoints = 0; for (var skillId in progressionData.upgrades) { var level = progressionData.upgrades[skillId]; var skillData = null; for (var category in talentTreeConfig) { var found = talentTreeConfig[category].find(function (s) { return s.id === skillId; }); if (found) { skillData = found; break; } } if (skillData) { spentPoints += level * skillData.costPerLevel; } } progressionData.points += spentPoints; progressionData.upgrades = {}; progressionData.pointsPerLevel = { '1': 0, '2': 0, '3': 0 }; pointsText.setText('Points: ' + progressionData.points); updateAllSkillVisuals(); if (descriptionPanel.visible) { updateDescriptionPanel(); } saveProgression(); }; var descriptionPanel; var skillNameText, skillDescriptionText, skillStatsText, upgradeButtonInPanel; var currentlySelectedSkillId = null; function createDescriptionPanel() { descriptionPanel = new Container(); descriptionPanel.x = 2048 / 2; descriptionPanel.y = 2732 - 350; descriptionPanel.visible = false; self.addChild(descriptionPanel); var panelBg = descriptionPanel.attachAsset('panel_skill_description_new', { anchorX: 0.5, anchorY: 0.5 }); panelBg.alpha = 0.8; skillNameText = new Text2("Skill Name", { size: 60, fill: 0xFFD700, weight: 700 }); skillNameText.anchor.set(0.5, 0); skillNameText.y = -238; descriptionPanel.addChild(skillNameText); skillDescriptionText = new Text2("Description text goes here.", { size: 60, fill: 0xffffff, weight: 800, wordWrap: true, wordWrapWidth: 1600, align: 'center' }); skillDescriptionText.anchor.set(0.5, 0); skillDescriptionText.y = -158; descriptionPanel.addChild(skillDescriptionText); skillStatsText = new Text2("Bonus: 10% -> 15%", { size: 55, fill: 0x00ff00 }); skillStatsText.anchor.set(0.5, 0); skillStatsText.y = -30; descriptionPanel.addChild(skillStatsText); upgradeButtonInPanel = new Container(); upgradeButtonInPanel.y = 180; descriptionPanel.addChild(upgradeButtonInPanel); var upgradeBtnBg = upgradeButtonInPanel.attachAsset('button_skill_upgrade', { anchorX: 0.5, anchorY: 0.5 }); upgradeButtonInPanel.down = function () { if (currentlySelectedSkillId) { upgradeSkill(currentlySelectedSkillId); } }; } function renderSkills(category) { nodeContainer.removeChildren(); skillNodes = {}; var skills = talentTreeConfig[category]; if (!skills) { return; } for (var i = 0; i < skills.length; i++) { var skillData = skills[i]; var node = new Container(); node.depth = 1; node.x = skillData.position.x; node.y = skillData.position.y; nodeContainer.addChild(node); var icon = node.attachAsset(skillData.icon, { anchorX: 0.5, anchorY: 0.5, width: 180, height: 180 }); var levelText = new Text2('0 / ' + skillData.maxLevels, { size: 60, fill: 0xffff00, weight: 900, stroke: 0x000000, // Kolor obrysu (czarny) strokeThickness: 5 // Grubość obrysu }); levelText.anchor.set(0.5, 1); levelText.y = -100; node.addChild(levelText); node.levelText = levelText; node.interactive = true; (function (data) { node.down = function () { showDescriptionPanel(data); }; })(skillData); skillNodes[skillData.id] = node; } for (var i = 0; i < skills.length; i++) { var skillData = skills[i]; var node = skillNodes[skillData.id]; if (skillData.dependencies && skillData.dependencies.length > 0) { skillData.dependencies.forEach(function (depId) { var depNode = skillNodes[depId]; if (depNode && node) { var dx = node.x - depNode.x; var dy = node.y - depNode.y; var distance = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); var lineShadow = nodeContainer.attachAsset('talent_tree_line', { anchorX: 0, anchorY: 0.5 }); lineShadow.depth = 0; lineShadow.width = distance; lineShadow.height = 8; lineShadow.rotation = angle; lineShadow.x = depNode.x; lineShadow.y = depNode.y; lineShadow.tint = 0x000000; lineShadow.alpha = 0.4; var line = nodeContainer.attachAsset('talent_tree_line', { anchorX: 0, anchorY: 0.5 }); line.depth = 0; line.width = distance; line.height = 4; line.rotation = angle; line.x = depNode.x; line.y = depNode.y; line.tint = 0xAAAAAA; line.alpha = 0.5; } }); } } nodeContainer.children.sort(function (a, b) { return (a.depth || 0) - (b.depth || 0); }); updateAllSkillVisuals(); } function showDescriptionPanel(skillData) { currentlySelectedSkillId = skillData.id; descriptionPanel.visible = true; skillNameText.setText(skillData.name); skillDescriptionText.setText(skillData.description); updateDescriptionPanel(); } function updateDescriptionPanel() { if (!currentlySelectedSkillId || !descriptionPanel.visible) { return; } var skillData = talentTreeConfig[currentCategory].find(function (s) { return s.id === currentlySelectedSkillId; }); var currentLevel = progressionData.upgrades[currentlySelectedSkillId] || 0; var statsString = "Level: " + currentLevel + " / " + skillData.maxLevels; if (currentLevel < skillData.maxLevels) { var currentValueMatch = skillData.description.match(/(\d+)%/); if (currentValueMatch) { var bonusPerLevel = parseFloat(currentValueMatch[1]); var currentValue = bonusPerLevel * currentLevel; var nextValue = bonusPerLevel * (currentLevel + 1); statsString += "\nBonus: " + currentValue + "% -> " + nextValue + "%"; } } skillStatsText.setText(statsString); var isMaxed = currentLevel >= skillData.maxLevels; upgradeButtonInPanel.visible = !isMaxed; if (!isMaxed) { var cost = skillData.costPerLevel; var canAfford = progressionData.points >= cost; var areDepsMet = true; if (skillData.dependencies && skillData.dependencies.length > 0) { areDepsMet = skillData.dependencies.every(function (depId) { var depSkillData = talentTreeConfig[currentCategory].find(function (s) { return s.id === depId; }); var requiredLevel = depSkillData ? Math.min(2, depSkillData.maxLevels) : 1; return (progressionData.upgrades[depId] || 0) >= requiredLevel; }); } upgradeButtonInPanel.alpha = canAfford && areDepsMet ? 1.0 : 0.5; } } function updateSkillVisuals(node, skillId) { var skillData = talentTreeConfig[currentCategory].find(function (s) { return s.id === skillId; }); var currentLevel = progressionData.upgrades[skillId] || 0; node.levelText.setText(currentLevel + ' / ' + skillData.maxLevels); var areDepsMet = true; if (skillData.dependencies && skillData.dependencies.length > 0) { areDepsMet = skillData.dependencies.every(function (depId) { var depSkillData = talentTreeConfig[currentCategory].find(function (s) { return s.id === depId; }); var requiredLevel = depSkillData ? Math.min(2, depSkillData.maxLevels) : 1; return (progressionData.upgrades[depId] || 0) >= requiredLevel; }); } var targetAlpha = areDepsMet ? 1.0 : 0.6; node.children.forEach(function (child) { child.alpha = targetAlpha; }); var icon = node.children[0]; if (currentLevel >= skillData.maxLevels) { icon.tint = 0xFFD700; } else if (currentLevel > 0) { icon.tint = 0xFFFFFF; // Zmieniono z 0x00BFFF na 0xFFFFFF } else { icon.tint = 0xFFFFFF; } if (!areDepsMet) { icon.tint = 0x888888; } } function upgradeSkill(skillId) { var skillData = talentTreeConfig[currentCategory].find(function (s) { return s.id === skillId; }); var currentLevel = progressionData.upgrades[skillId] || 0; var areDepsMet = true; if (skillData.dependencies && skillData.dependencies.length > 0) { areDepsMet = skillData.dependencies.every(function (depId) { var depSkillData = talentTreeConfig[currentCategory].find(function (s) { return s.id === depId; }); var requiredLevel = depSkillData ? Math.min(2, depSkillData.maxLevels) : 1; return (progressionData.upgrades[depId] || 0) >= requiredLevel; }); } if (areDepsMet && progressionData.points >= skillData.costPerLevel && currentLevel < skillData.maxLevels) { progressionData.points -= skillData.costPerLevel; progressionData.upgrades[skillId] = (progressionData.upgrades[skillId] || 0) + 1; pointsText.setText('Points: ' + progressionData.points); updateAllSkillVisuals(); updateDescriptionPanel(); saveProgression(); } } function updateAllSkillVisuals() { for (var skillId in skillNodes) { updateSkillVisuals(skillNodes[skillId], skillId); } } function showCategory(category) { currentCategory = category; categoryTabs.forEach(function (tab) { var targetScale = tab.categoryName === category ? 1.15 : 1.0; tween(tab, { scaleX: targetScale, scaleY: targetScale }, { duration: 200, easing: tween.easeOut }); }); if (descriptionPanel) { descriptionPanel.visible = false; currentlySelectedSkillId = null; } for (var key in backgrounds) { backgrounds[key].visible = key === category; } renderSkills(category); } var categories = Object.keys(talentTreeConfig); var tabSpacing = 310; var firstTabX = 1750; categories.reverse().forEach(function (categoryName, index) { var tab = new Container(); tab.categoryName = categoryName; tab.x = firstTabX - index * tabSpacing; tab.y = 100; var assetName = 'button_category_' + categoryName.toLowerCase(); var tabBg = tab.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.addChild(tab); categoryTabs.push(tab); tab.down = function () { showCategory(categoryName); }; }); createDescriptionPanel(); self.show = function () { self.visible = true; pointsText.setText('Points: ' + progressionData.points); showCategory(categories[4]); }; self.hide = function () { self.visible = false; if (descriptionPanel) { descriptionPanel.visible = false; currentlySelectedSkillId = null; } if (titleScreenContainer) { titleScreenContainer.visible = true; } }; 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 lifetime = spellData.duration || 90; var hitEnemies = []; var totalStrikes = 12; var stormDuration = 90; var strikesSpawned = 0; var spawnTicker = 0; var timeBetweenStrikes = stormDuration / totalStrikes; self.update = function () { lifetime--; if (lifetime <= 0) { self.destroy(); return; } enemies.forEach(function (enemy) { if (hitEnemies.indexOf(enemy) !== -1) { return; } 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; if (spellData.damage) { enemy.takeDamage(spellData.damage); } hitEnemies.push(enemy); } }); spawnTicker++; if (spawnTicker >= timeBetweenStrikes) { spawnTicker = 0; strikesSpawned++; if (strikesSpawned > totalStrikes) { 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.targetEnemies = []; self.lastFired = 0; self.totalValue = 0; self.isConstructing = false; self.constructionContainer = null; self.timerText = null; self.auraEffectContainer = null; self.baseStats = {}; self.buffs = []; self.tileBuff = null; // NOWA WŁAŚCIWOŚĆ 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 () { var baseStats = TOWER_DATA[self.towerType][self.level + self.levelPath]; var newDamage = baseStats.damage; var newFireRate = baseStats.fireRate; var newRange = baseStats.range; if (progressionData.upgrades['towers_cost_1']) {} if (self.towerType === 'guard' && progressionData.upgrades['towers_guard_dmg_2']) { newDamage *= 1 + progressionData.upgrades['towers_guard_dmg_2'] * 0.05; } if (self.towerType === 'crossbow' && progressionData.upgrades['towers_crossbow_range_2']) { newRange *= 1 + progressionData.upgrades['towers_crossbow_range_2'] * 0.05; } if (self.towerType === 'guard' && progressionData.upgrades['towers_guard_as_4']) { newFireRate *= 0.70; } // --- NOWY FRAGMENT - START --- if (self.towerType === 'mage' && progressionData.upgrades['towers_mage_potency_1']) { newDamage *= 1 + progressionData.upgrades['towers_mage_potency_1'] * 0.05; // +5% na poziom } // --- NOWY FRAGMENT - KONIEC --- var hasSmithy = castleBuildings.some(function (b) { return b.id === 'smithy'; }); if (hasSmithy) { var infraBonus = 1 + (progressionData.upgrades['building_infrastructure_2'] || 0) * 0.05; newDamage *= 1.15 * infraBonus; } self.baseStats.damage = newDamage; self.baseStats.fireRate = newFireRate; self.baseStats.range = newRange; var finalDamage = self.baseStats.damage; var finalFireRate = self.baseStats.fireRate; if (self.tileBuff) { if (self.tileBuff.damage) { finalDamage *= self.tileBuff.damage; } if (self.tileBuff.attackSpeed) { finalFireRate /= self.tileBuff.attackSpeed; } } self.buffs.forEach(function (buff) { if (buff.effect.damage) { finalDamage *= buff.effect.damage; } if (buff.effect.attackSpeed) { finalFireRate /= buff.effect.attackSpeed; } }); self.damage = finalDamage; self.fireRate = finalFireRate; self.range = self.baseStats.range; }; 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) { levelProgressFlags.maxTowerLevelUsed = Math.max(levelProgressFlags.maxTowerLevelUsed, self.level + 1); 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); // Stosuje bazowe staty i przelicza z buffami 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 - 50; if (self.towerType === 'crossbow' && self.gridX < 12) { var towerGraphic = self.children[0]; if (towerGraphic) { towerGraphic.scale.x = -1; } } // Sprawdź bonus z pola i go przypisz 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.specialEffect) { if (cell.specialEffect === 'amplifier') { self.tileBuff = { damage: 1.15 }; } else if (cell.specialEffect === 'catalyst') { self.tileBuff = { attackSpeed: 1.30 }; } self.recalculateStats(); break; // Znaleziono bonus, przerywamy pętlę } } if (self.tileBuff) { break; } } for (var i = 0; i < 2; i++) { for (var j = 0; j < 2; j++) { var cellToOccupy = grid.getCell(gridX + i, gridY + j); if (cellToOccupy) { cellToOccupy.type = 5; } } } if (self.aura) { self.applyAuraToTowers(); } }; self.findTargets = function () { // NOWA LOGIKA: Uwzględnienie dodatkowego strzału var maxTargets = self.multiShot || 1; if (self.towerType === 'guard' && progressionData.upgrades['towers_guard_multishot_3']) { var extraShotChance = progressionData.upgrades['towers_guard_multishot_3'] * 0.10; if (Math.random() < extraShotChance) { maxTargets++; } } var potentialTargets = []; if (self.damage === 0) { return []; } if (self.bulletType === 'slow') { var unslowedTargets = []; var slowedTargets = []; 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) { if (!enemy.slowed) { unslowedTargets.push(enemy); } else { slowedTargets.push(enemy); } } }); potentialTargets = unslowedTargets.concat(slowedTargets); } else { 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); } }); potentialTargets.sort(function (a, b) { var distA = Math.pow(a.x - self.x, 2) + Math.pow(a.y - self.y, 2); var distB = Math.pow(b.x - self.x, 2) + Math.pow(b.y - self.y, 2); return distA - distB; }); } return potentialTargets.slice(0, maxTargets); }; self.fire = function () { self.targetEnemies.forEach(function (targetEnemy) { if (targetEnemy && targetEnemy.health > 0) { var dx = targetEnemy.x - self.x; var dy = 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 bulletProps = { damage: self.damage, range: self.range, bulletSpeed: self.bulletSpeed, graphic: self.graphic, bulletType: self.bulletType, splashDamage: self.splashDamage, splashRadius: self.splashRadius, splashScale: self.splashScale, slowAmount: self.slowAmount, slowDuration: self.slowDuration, slowChance: self.slowChance, chainTargets: self.chainTargets, chainDamageFalloff: self.chainDamageFalloff, chainChance: self.chainChance, poisonChance: self.poisonChance, sourceTowerType: self.towerType, stunChance: self.stunChance, chainStunChance: self.chainStunChance, stunDuration: self.stunDuration }; if (self.towerType === 'crossbow' && progressionData.upgrades['towers_crossbow_crit_4']) { if (Math.random() < 0.20) { bulletProps.damage *= 2; } } if (self.towerType === 'crossbow' && progressionData.upgrades['towers_crossbow_poison_3']) { bulletProps.bulletType = 'poison'; bulletProps.poisonChance = progressionData.upgrades['towers_crossbow_poison_3'] * 0.10; } var bullet = new Bullet(bulletX, bulletY, targetEnemy, bulletProps); 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 + 20; self.addChild(self.timerText); self.constructionContainer.y = 30; 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.targetEnemies = self.findTargets(); if (self.targetEnemies.length > 0) { 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 clickBlocker = new Container(); clickBlocker.isUpgradeBlocker = true; clickBlocker.attachAsset('cell', { width: 2048, height: 2732, alpha: 0.01 }); clickBlocker.interactive = true; clickBlocker.down = function () { closeActiveUpgradeMenu(); }; game.addChild(clickBlocker); // NOWA LINIA: Ukrycie dolnego panelu budowania if (mainActionPanel) { mainActionPanel.y = 2732 + 100; } var upgradeMenu = game.addChild(new UpgradeMenu(self)); upgradeMenu.interactive = true; upgradeMenu.x = 2048 / 2; isMenuTransitioning = true; tween(upgradeMenu, { y: 2732 - 250 }, { duration: 200, easing: tween.backOut, onFinish: function onFinish() { isMenuTransitioning = false; } }); if (mainActionPanel) { mainActionPanel.interactive = false; } if (castleBuildPanel) { castleBuildPanel.interactive = 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 UnitspediaScreen = Container.expand(function () { var self = Container.call(this); self.visible = false; var currentCategory = null; var bookContainer = new Container(); bookContainer.x = 2048 / 2; bookContainer.y = 2732 / 2; bookContainer.visible = false; self.addChild(bookContainer); var background = bookContainer.attachAsset('unitspedia_bg', { anchorX: 0.5, anchorY: 0.5 }); var categoryTabs = []; var unitButtons = []; var unitSelectionContainer = new Container(); bookContainer.addChild(unitSelectionContainer); var detailsContainer = new Container(); bookContainer.addChild(detailsContainer); var categoryContainer = new Container(); self.addChild(categoryContainer); var backButton = new Container(); backButton.x = 2048 - 180; backButton.y = 100; var backButtonBg = backButton.attachAsset('button_back_encyclopedia', { anchorX: 0.5, anchorY: 0.5 }); self.addChild(backButton); backButton.down = function () { self.hide(); }; function showUnitDetails(unitData) { detailsContainer.removeChildren(); if (!unitData) { return; } var unitImage = detailsContainer.attachAsset(unitData.image, { anchorX: 0.5, anchorY: 0.5 }); unitImage.x = -440; unitImage.y = 150; unitImage.width = 500; unitImage.height = 500; var statsContainer = new Container(); statsContainer.x = -640; statsContainer.y = 250; detailsContainer.addChild(statsContainer); var statTextStyle = { size: 60, fill: 0x3b2d24, weight: 600 }; var baseStats = null; if (UNIT_DATA[unitData.id]) { baseStats = UNIT_DATA[unitData.id]; } else if (HERO_DATA[unitData.id]) { baseStats = HERO_DATA[unitData.id]; } else { switch (unitData.id) { case 'orc_normal': baseStats = { maxHealth: 75, damage: 4, speed: 1 }; break; case 'orc_shield': baseStats = { maxHealth: 120, damage: 3, speed: 1 }; break; case 'orc_rider': baseStats = { maxHealth: 100, damage: 7, speed: 1.5 }; break; case 'orc_archer': baseStats = { maxHealth: 50, damage: 9, speed: 1 }; break; case 'orc_chieftain': baseStats = { maxHealth: 550, damage: 14, speed: 1 }; break; case 'undead_soldier': baseStats = { maxHealth: 110, damage: 5, speed: 0.9 }; break; case 'undead_archer': baseStats = { maxHealth: 70, damage: 11, speed: 1.0 }; break; case 'undead_horseman': baseStats = { maxHealth: 130, damage: 8, speed: 1.5 }; break; case 'death_knight': baseStats = { maxHealth: 380, damage: 11, speed: 0.8 }; break; case 'undead_shielder': baseStats = { maxHealth: 160, damage: 3, speed: 0.8 }; break; case 'lich': baseStats = { maxHealth: 400, damage: 4, speed: 0.5 }; break; case 'demon_soldier': baseStats = { maxHealth: 40, damage: 4, speed: 1.0 }; break; case 'demon_archer': baseStats = { maxHealth: 30, damage: 7, speed: 1.0 }; break; case 'demon_defender': baseStats = { maxHealth: 80, damage: 2, speed: 0.8 }; break; case 'demon_rider': baseStats = { maxHealth: 50, damage: 6, speed: 1.6 }; break; case 'demon_lord': baseStats = { maxHealth: 200, damage: 12, speed: 1.0 }; break; case 'demon_tamer': baseStats = { maxHealth: 300, damage: 8, speed: 1.0 }; break; } } if (baseStats) { var health = new Text2("Health: " + baseStats.maxHealth, statTextStyle); health.y = 200; statsContainer.addChild(health); var damage = new Text2("Damage: " + baseStats.damage, statTextStyle); damage.y = 280; statsContainer.addChild(damage); var speed = new Text2("Speed: " + baseStats.speed, statTextStyle); speed.y = 360; statsContainer.addChild(speed); } var descriptionContainer = new Container(); descriptionContainer.x = 92; descriptionContainer.y = -250; detailsContainer.addChild(descriptionContainer); var descriptionText = new Text2(unitData.description, { size: 60, fill: 0x3b2d24, weight: 500, align: 'left', wordWrap: true, wordWrapWidth: 700 }); descriptionText.anchor.set(0, 0); descriptionContainer.addChild(descriptionText); } function showCategory(categoryName) { currentCategory = categoryName; categoryTabs.forEach(function (tab) { var targetScale = tab.categoryName === categoryName ? 1.1 : 1.0; var targetY = tab.categoryName === categoryName ? 230 : 250; tween(tab, { scaleX: targetScale, scaleY: targetScale, y: targetY }, { duration: 200, easing: tween.easeOut }); }); unitSelectionContainer.removeChildren(); detailsContainer.removeChildren(); unitButtons = []; var units = UNITSPEDIA_DATA[categoryName]; if (!units) { return; } var standardUnits = units.filter(function (u) { return !u.name.startsWith('Hero:'); }); var heroUnits = units.filter(function (u) { return u.name.startsWith('Hero:'); }); var startY = -600; var spacingY = 100; standardUnits.forEach(function (unitData, index) { createUnitButton(unitData, startY + index * spacingY); }); var heroStartY = 800; heroUnits.forEach(function (unitData, index) { createUnitButton(unitData, heroStartY + index * spacingY); }); function createUnitButton(unitData, yPos) { var unitButton = new Container(); unitButton.unitData = unitData; var assetName; switch (categoryName) { case 'Humans': assetName = 'unitspedia_btn_humans'; break; case 'Orcs': assetName = 'unitspedia_btn_orcs'; break; case 'Undead': assetName = 'unitspedia_btn_undead'; break; case 'Demons': assetName = 'unitspedia_btn_demons'; break; default: assetName = 'unitspedia_unit_btn'; break; } var buttonBg = unitButton.attachAsset(assetName, { anchorX: 0, anchorY: 0.5 }); var unitText = new Text2(unitData.name, { size: 55, fill: 0x3b2d24, weight: 'bold', stroke: 0xFFF8DC, strokeThickness: 1 }); unitText.anchor.set(0, 0.5); unitText.x = 25; unitButton.textObject = unitText; unitButton.addChild(unitText); unitButton.x = -850; unitButton.y = yPos; unitSelectionContainer.addChild(unitButton); unitButtons.push(unitButton); (function (button) { button.down = function () { showUnitDetails(button.unitData); unitButtons.forEach(function (btn) { btn.textObject.setText(btn.unitData.name); btn.scale.set(1.0); }); button.textObject.setText("> " + button.unitData.name); button.scale.set(1.05); }; })(unitButton); } if (units.length > 0) { showUnitDetails(units[0]); if (unitButtons.length > 0) { unitButtons[0].textObject.setText("> " + units[0].name); unitButtons[0].scale.set(1.05); } } } var categories = ['Humans', 'Orcs', 'Undead', 'Demons']; var bannerWidth = 200; var bannerSpacing = 80; var totalWidth = categories.length * bannerWidth + (categories.length - 1) * bannerSpacing; var startX = (2048 - totalWidth) / 2 + bannerWidth / 2; categories.forEach(function (categoryName, index) { var tab = new Container(); tab.categoryName = categoryName; var assetName; switch (categoryName) { case 'Humans': assetName = 'banner_humans'; break; case 'Orcs': assetName = 'banner_orcs'; break; case 'Undead': assetName = 'banner_undead'; break; case 'Demons': assetName = 'banner_demons'; break; } if (assetName) { var tabBg = tab.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); tab.x = startX + index * (bannerWidth + bannerSpacing); tab.y = 250; categoryContainer.addChild(tab); categoryTabs.push(tab); tab.down = function () { showCategory(categoryName); }; } }); self.show = function () { self.visible = true; bookContainer.visible = true; if (categories.length > 0) { showCategory(categories[0]); } }; self.hide = function () { self.visible = false; bookContainer.visible = false; if (titleScreenContainer) { titleScreenContainer.visible = true; } }; return self; }); var UpgradeMenu = Container.expand(function (tower) { var self = Container.call(this); self.tower = tower; self.y = 2732 + 225; var menuBackground = self.attachAsset('panel_upgrade_background', { anchorX: 0.5, anchorY: 0.5 }); menuBackground.width = 1800; menuBackground.height = 550; 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 createStatLine(label, value, yPos, isBuffed) { var statLabel = new Text2(label + ':', { size: 55, fill: 0xFFFFFF, weight: 600, align: 'left' }); statLabel.anchor.set(0, 0.5); statLabel.x = -620; statLabel.y = yPos; self.addChild(statLabel); var statValue = new Text2(value, { size: 55, fill: isBuffed ? 0x00FF00 : 0xFFD700, weight: 800, align: 'left' }); statValue.anchor.set(0, 0.5); statValue.x = statLabel.x + 300; statValue.y = yPos; self.addChild(statValue); } function populateStats() { var yStart = -120; var yStep = 70; var isDamageBuffed = self.tower.tileBuff && self.tower.tileBuff.damage; var isSpeedBuffed = self.tower.tileBuff && self.tower.tileBuff.attackSpeed; createStatLine('Damage', Math.ceil(self.tower.damage), yStart, isDamageBuffed); var fireRateDisplay = self.tower.fireRate > 0 ? Math.round(60 / self.tower.fireRate * 100) / 100 + '/s' : 'N/A'; createStatLine('Fire Rate', fireRateDisplay, yStart + yStep, isSpeedBuffed); createStatLine('Range', Math.round(self.tower.range / CELL_SIZE * 10) / 10, yStart + yStep * 2, false); } if (self.tower.towerType === 'banner') { var towerData = TOWER_DATA[self.tower.towerType][self.tower.level + self.tower.levelPath]; var descriptionText = new Text2(towerData.description, { size: 55, fill: 0xFFFFFF, weight: 600, align: 'left', wordWrap: true, wordWrapWidth: 600 }); descriptionText.anchor.set(0, 0.5); descriptionText.x = -620; descriptionText.y = -50; self.addChild(descriptionText); } else { populateStats(); } function createUpgradeButton(upgradeKey, position, totalButtons) { var upgradeData = TOWER_DATA[self.tower.towerType][upgradeKey]; if (!upgradeData) { return; } var finalCost = upgradeData.cost; if (castleBuildings.some(function (b) { return b.id === 'workshop'; })) { if (progressionData.upgrades['building_workshop_3']) { var costReduction = 1 - progressionData.upgrades['building_workshop_3'] * 0.05; finalCost = Math.floor(finalCost * costReduction); } } var button = new Container(); var bg = button.attachAsset('button_upgrade', { anchorX: 0.5, anchorY: 0.5 }); bg.width = 500; if (totalButtons > 1) { bg.height = (450 - 100) / totalButtons - 15; } else { bg.height = 140; } var buttonTextContent = upgradeData.name + '\n' + finalCost + 'g'; var text = new Text2(buttonTextContent, { size: 40, fill: 0xFFFFFF, weight: 800, align: 'center', wordWrap: true, wordWrapWidth: bg.width - 20 }); text.anchor.set(0.5, 0.5); text.x = 20; button.addChild(text); var canAfford = gold >= finalCost; button.alpha = canAfford ? 1 : 0.7; button.down = function () { if (gold >= finalCost) { setGold(gold - finalCost); 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) && self.tower.towerType !== 'banner') { var maxLevelText = new Text2('Max Level', { size: 60, 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 = 300; self.addChild(maxLevelText); return; } if (self.tower.towerType === 'banner' && self.tower.level >= 2) { var maxLevelText = new Text2('Max Level', { size: 60, fill: 0xFFD700, weight: 800 }); maxLevelText.anchor.set(0.5, 0.5); maxLevelText.x = 300; 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 (self.tower.level === 2 && self.tower.levelPath && !key.includes(self.tower.levelPath)) { return false; } if (nextLevel >= 3 && !hasWorkshop && self.tower.towerType !== 'banner') { return false; } return true; }); var buttonX = 300; if (upgradeKeys.length === 1) { createUpgradeButton(upgradeKeys[0], { x: buttonX, y: 0 }, 1); } else if (upgradeKeys.length > 1) { var totalButtonHeight = 450 - 100; var buttonHeight = totalButtonHeight / upgradeKeys.length; var startY = -totalButtonHeight / 2 + buttonHeight / 2; for (var i = 0; i < upgradeKeys.length; i++) { createUpgradeButton(upgradeKeys[i], { x: buttonX, y: startY + i * buttonHeight }, upgradeKeys.length); } } } populateUpgradeButtons(); 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) { gateContainer.removeChild(game.children[i]); } } if (selectedTower === self.tower) { selectedTower = null; } originalDestroy.call(self); }; return self; }); var WaveNotification = Container.expand(function (message) { var self = Container.call(this); var textStyle = { size: 110, fill: 0xFFD700, weight: 800, align: 'center', stroke: 0x000000, strokeThickness: 10, wordWrap: true, wordWrapWidth: 2000 }; var notificationText = new Text2(message, textStyle); notificationText.anchor.set(0.5, 0.5); self.addChild(notificationText); self.alpha = 0; self.scale.set(0.5); self.x = 2048 / 2; self.y = 2732 / 2; tween(self, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { LK.setTimeout(function () { tween(self, { alpha: 0, scaleX: 0.7, scaleY: 0.7 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); }, 2000); } }); return self; }); var WeatherSelection = Container.expand(function (levelNumber, onComplete) { var self = Container.call(this); self.levelNumber = levelNumber; self.onComplete = onComplete; self.cards = []; self.animationTicker = 0; self.animationDelay = 5; self.currentStep = 0; self.winningCard = null; self.winningCardIndex = -1; self.stepsToGo = -1; var bg = self.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000, alpha: 0 }); tween(bg, { alpha: 0.7 }, { duration: 300 }); var visualDeck = WEATHER_DECK_DATA[self.levelNumber] || WEATHER_DECK_DATA[1]; var cardContainer = new Container(); self.addChild(cardContainer); var cardSpacing = 240; var totalWidth = (visualDeck.length - 1) * cardSpacing; var startX = -totalWidth / 2; cardContainer.x = 2048 / 2; cardContainer.y = 2732 / 2 - 200; for (var i = 0; i < visualDeck.length; i++) { var eventId = visualDeck[i]; var cardAsset = EVENT_DATA[eventId].asset; var card = cardContainer.attachAsset(cardAsset, { anchorX: 0.5, anchorY: 0.5 }); card.x = startX + i * cardSpacing; card.y = 500; card.alpha = 0; card.tint = 0x777777; card.eventId = eventId; self.cards.push(card); var tweenOptions = { duration: 400, delay: 100 + i * 150, easing: tween.backOut }; if (i === visualDeck.length - 1) { tweenOptions.onFinish = function () { LK.setTimeout(function () { if (self.startSelection) { self.startSelection(); } }, 1000); }; } tween(card, { y: 0, alpha: 1 }, tweenOptions); } self.startSelection = function () { self.winningCardIndex = Math.floor(Math.random() * self.cards.length); self.winningCard = self.cards[self.winningCardIndex]; var fullLoops = 2; self.stepsToGo = fullLoops * self.cards.length + self.winningCardIndex; }; self.showResult = function () { activeWeatherEvent = self.winningCard.eventId; applyActiveWeatherModifiers(); self.winningCard.tint = 0xFFFFFF; var flash = self.attachAsset('cell', { width: 2048, height: 2732, tint: 0xFFFFFF, alpha: 0 }); tween(flash, { alpha: 0.7 }, { duration: 100, onFinish: function onFinish() { tween(flash, { alpha: 0 }, { duration: 150, onFinish: function onFinish() { flash.destroy(); } }); } }); shakeScreen(15, 200); for (var i = 0; i < 30; i++) { var particle = self.attachAsset('star_particle', { anchorX: 0.5, anchorY: 0.5 }); particle.x = self.winningCard.x + cardContainer.x + (Math.random() - 0.5) * 200; particle.y = self.winningCard.y + cardContainer.y + (Math.random() - 0.5) * 200; particle.scale.set(Math.random() * 0.5 + 0.3); tween(particle, { x: particle.x + (Math.random() - 0.5) * 400, y: particle.y + (Math.random() - 0.5) * 400, alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 800 + Math.random() * 500, onFinish: particle.destroy.bind(particle) }); } for (var i = 0; i < self.cards.length; i++) { if (i !== self.winningCardIndex) { tween(self.cards[i], { alpha: 0, y: 50 }, { duration: 300 }); } } tween(self.winningCard, { x: 0, y: -100, scaleX: 1.5, scaleY: 1.5 }, { duration: 800, easing: tween.easeOut }); var eventInfo = EVENT_DATA[self.winningCard.eventId]; var descriptionContainer = new Container(); descriptionContainer.x = cardContainer.x; descriptionContainer.y = cardContainer.y + 400; descriptionContainer.alpha = 0; self.addChild(descriptionContainer); var nameText = new Text2(eventInfo.name, { size: 73, fill: 0xFFFFFF, weight: 800 }); nameText.anchor.set(0.5, 0.5); nameText.y = -60; var descText = new Text2(eventInfo.description, { size: 56, fill: 0xDDDDDD, weight: 500, align: 'center', wordWrap: true, wordWrapWidth: 800 }); descText.anchor.set(0.5, 0.5); descText.y = 90; descriptionContainer.addChild(nameText); descriptionContainer.addChild(descText); tween(descriptionContainer, { alpha: 1 }, { duration: 400, delay: 200 }); LK.setTimeout(function () { tween(self, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { if (self.onComplete) { self.onComplete(); } self.destroy(); } }); }, 4000); }; self.update = function () { if (self.stepsToGo > -1) { self.animationTicker++; if (self.animationTicker >= self.animationDelay) { self.animationTicker = 0; self.cards.forEach(function (card) { card.tint = 0x777777; }); var currentIndex = self.currentStep % self.cards.length; self.cards[currentIndex].tint = 0xFFFFFF; if (self.currentStep >= self.stepsToGo) { self.stepsToGo = -1; self.cards.forEach(function (card) { card.tint = 0x777777; }); self.cards[self.winningCardIndex].tint = 0xFFFFFF; LK.setTimeout(function () { self.showResult(); }, 1000); } else { self.currentStep++; if (self.stepsToGo - self.currentStep < 5) { self.animationDelay += 4; } else if (self.stepsToGo - self.currentStep < 10) { self.animationDelay += 1; } } } } }; return self; }); /**** * Initialize Game ****/ /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Placeholder dla napisu /**** * Game Code ****/ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } LK.import("@upit/storage.v1"); var progressionData = { points: 0, upgrades: {}, pointsPerLevel: { '1': 0, '2': 0, '3': 0 }, hellgatesHighScore: 0, achievements: {} }; function saveProgression() { storage.progression_points = progressionData.points; storage.progression_upgrades = progressionData.upgrades; storage.progression_pointsPerLevel = progressionData.pointsPerLevel; storage.hellgates_high_score = progressionData.hellgatesHighScore; storage.progression_achievements = progressionData.achievements; } function loadProgression() { var loadedPoints = storage.progression_points; var loadedUpgrades = storage.progression_upgrades; var loadedPointsPerLevel = storage.progression_pointsPerLevel; var loadedHighScore = storage.hellgates_high_score; var loadedAchievements = storage.progression_achievements; progressionData.points = typeof loadedPoints === 'number' ? loadedPoints : 0; progressionData.upgrades = loadedUpgrades && _typeof(loadedUpgrades) === 'object' ? loadedUpgrades : {}; progressionData.pointsPerLevel = loadedPointsPerLevel && _typeof(loadedPointsPerLevel) === 'object' ? loadedPointsPerLevel : { '1': 0, '2': 0, '3': 0 }; progressionData.hellgatesHighScore = typeof loadedHighScore === 'number' ? loadedHighScore : 0; progressionData.achievements = loadedAchievements && _typeof(loadedAchievements) === 'object' ? loadedAchievements : {}; } var talentTreeConfig = { 'Towers': [{ id: 'towers_cost_1', name: 'Efficient Builders', icon: 'icon_efficient_builders', description: 'Reduces the build cost of all towers by 2%', maxLevels: 5, costPerLevel: 1, position: { x: 1024, y: 650 }, dependencies: [] }, { id: 'towers_mage_potency_1', name: 'Mystic Amplification', icon: 'icon_mystic_amplification', description: 'Increases the damage of all Mage Towers by 5%', maxLevels: 4, costPerLevel: 1, position: { x: 1024, y: 1000 }, dependencies: ['towers_cost_1'] }, { id: 'towers_mage_essence_tap_2', name: 'Mana Leech', icon: 'icon_mana_leech', description: 'Killing an enemy with a Mage Tower restores 1 mana.', maxLevels: 1, costPerLevel: 2, position: { x: 1024, y: 1350 }, dependencies: ['towers_mage_potency_1'] }, { id: 'towers_guard_dmg_2', name: 'Reinforced Arrows', icon: 'icon_reinforced_arrows', description: 'Increases Guard Tower damage by 5%', maxLevels: 3, costPerLevel: 1, position: { x: 624, y: 1000 }, dependencies: ['towers_cost_1'] }, { id: 'towers_crossbow_range_2', name: 'Eagle Eye', icon: 'icon_eagle_eye', description: 'Increases Crossbow Tower range by 5%', maxLevels: 3, costPerLevel: 1, position: { x: 1424, y: 1000 }, dependencies: ['towers_cost_1'] }, { id: 'towers_guard_multishot_3', name: 'Hail of Arrows', icon: 'icon_hail_of_arrows', description: 'Guard Tower gains a 10% chance to fire an additional arrow', maxLevels: 3, costPerLevel: 1, position: { x: 424, y: 1350 }, dependencies: ['towers_guard_dmg_2'] }, { id: 'towers_crossbow_poison_3', name: 'Poisoned Bolts', icon: 'icon_poisoned_bolts', description: 'Crossbow Tower attacks have a 10% chance to poison the target', maxLevels: 3, costPerLevel: 1, position: { x: 1624, y: 1350 }, dependencies: ['towers_crossbow_range_2'] }, { id: 'towers_guard_as_4', name: 'Berserker\'s Fury', icon: 'icon_berserkers_fury', description: 'Increases Guard Tower attack speed by 30%', maxLevels: 1, costPerLevel: 3, position: { x: 424, y: 1700 }, dependencies: ['towers_guard_multishot_3'] }, { id: 'towers_crossbow_crit_4', name: 'Heartseeker Shot', icon: 'icon_heartseeker_shot', description: 'Crossbow Tower gains a 20% chance to deal 200% damage', maxLevels: 1, costPerLevel: 3, position: { x: 1624, y: 1700 }, dependencies: ['towers_crossbow_poison_3'] }], 'Building': [{ id: 'building_cost_1', name: 'Master Carpenter', icon: 'icon_master_carpenter', description: 'Reduces the build cost of all castle buildings by 4%', maxLevels: 5, costPerLevel: 1, position: { x: 1024, y: 650 }, dependencies: [] }, { id: 'building_tavern_2', name: 'Golden Enterprise', icon: 'icon_golden_enterprise', description: 'Increases income from Taverns by 10%', maxLevels: 5, costPerLevel: 1, position: { x: 450, y: 1000 }, dependencies: ['building_cost_1'] }, { id: 'building_infrastructure_2', name: 'Military Infrastructure', icon: 'icon_military_infrastructure', description: 'Increases the effectiveness of Smithy and Training Ground by 5%', maxLevels: 2, costPerLevel: 2, position: { x: 1024, y: 1000 }, dependencies: ['building_cost_1'] }, { id: 'building_population_2', name: 'Social Development', icon: 'icon_social_development', description: 'Increases population from each Home building by +1', maxLevels: 5, costPerLevel: 1, position: { x: 1600, y: 1000 }, dependencies: ['building_cost_1'] }, { id: 'building_reroll_3', name: 'Haggling', icon: 'icon_haggling', description: 'Reduces the cost of rerolling buildings by 25%', maxLevels: 2, costPerLevel: 1, position: { x: 450, y: 1350 }, dependencies: ['building_tavern_2'] }, { id: 'building_workshop_3', name: 'Military Innovations', icon: 'icon_military_innovations', description: 'Reduces the cost of Workshop upgrades by 5%', maxLevels: 3, costPerLevel: 1, position: { x: 1024, y: 1350 }, dependencies: ['building_infrastructure_2'] }, { id: 'building_infirmary_3', name: 'Medical Care', icon: 'icon_medical_care', description: 'Infirmary speeds up population regeneration by an additional 10%', maxLevels: 3, costPerLevel: 1, position: { x: 1600, y: 1350 }, dependencies: ['building_population_2'] }, { id: 'building_gold_4', name: 'Royal Grant', icon: 'icon_royal_grant', description: 'Start each game with +500 gold', maxLevels: 1, costPerLevel: 5, position: { x: 450, y: 1700 }, dependencies: ['building_reroll_3'] }, { id: 'building_lives_4', name: 'Reinforced Walls', icon: 'icon_reinforced_walls', description: 'Start each game with +10 lives', maxLevels: 1, costPerLevel: 5, position: { x: 1600, y: 1700 }, dependencies: ['building_infirmary_3'] }], 'Army': [{ id: 'army_cost_1', name: 'Barracks Management', icon: 'icon_barracks_management', description: 'Reduces the recruitment cost of all units by 2%', maxLevels: 5, costPerLevel: 1, position: { x: 1024, y: 650 }, dependencies: [] }, { id: 'army_swordsman_hp_2', name: 'Infantry Vigor', icon: 'icon_infantry_vigor', description: 'Increases Swordsman HP by 5%', maxLevels: 4, costPerLevel: 1, position: { x: 450, y: 1000 }, dependencies: ['army_cost_1'] }, { id: 'army_horseman_stats_2', name: 'Cavalry Conditioning', icon: 'icon_cavalry_conditioning', description: 'Increases Horseman HP and Damage by 3%', maxLevels: 4, costPerLevel: 1, position: { x: 1024, y: 1000 }, dependencies: ['army_cost_1'] }, { id: 'army_free_unit_2', name: 'Surprise Draft', icon: 'icon_surprise_draft', description: 'Grants a 5% chance to get a free, additional unit on recruitment', maxLevels: 3, costPerLevel: 2, position: { x: 1600, y: 1000 }, dependencies: ['army_cost_1'] }, { id: 'army_defender_hp_3', name: 'Steel Shields', icon: 'icon_steel_shields', description: 'Increases Defender HP by 5%', maxLevels: 3, costPerLevel: 1, position: { x: 450, y: 1350 }, dependencies: ['army_swordsman_hp_2'] }, { id: 'army_archer_dmg_3', name: 'Precise Shots', icon: 'icon_precise_shots', description: 'Increases Archer Damage by 5%', maxLevels: 3, costPerLevel: 1, position: { x: 824, y: 1350 }, dependencies: ['army_horseman_stats_2'] }, { id: 'army_horseman_charge_3', name: 'Crushing Charge', icon: 'icon_crushing_charge', description: 'First attack of a Horseman slows the target by 15%', maxLevels: 3, costPerLevel: 1, position: { x: 1224, y: 1350 }, dependencies: ['army_horseman_stats_2'] }, { id: 'army_swordsman_ultimate_4', name: 'Undying Warrior', icon: 'icon_undying_warrior', description: 'Swordsman deals +30% damage and fights for 2s after receiving a fatal blow', maxLevels: 1, costPerLevel: 3, position: { x: 300, y: 1700 }, dependencies: ['army_defender_hp_3'] }, { id: 'army_defender_ultimate_4', name: 'Stunning Blow', icon: 'icon_stunning_blow', description: 'Defender attacks have a 20% chance to stun the target for 2s', maxLevels: 1, costPerLevel: 3, position: { x: 600, y: 1700 }, dependencies: ['army_defender_hp_3'] }, { id: 'army_archer_ultimate_4', name: 'Salvo', icon: 'icon_salvo', description: 'Archers always fire at two targets simultaneously', maxLevels: 1, costPerLevel: 3, position: { x: 824, y: 1700 }, dependencies: ['army_archer_dmg_3'] }, { id: 'army_horseman_ultimate_4', name: 'Ghost Rider', icon: 'icon_ghost_rider', description: 'When a Horseman dies, it has a 40% chance to spawn a Swordsman in its place', maxLevels: 1, costPerLevel: 3, position: { x: 1224, y: 1700 }, dependencies: ['army_horseman_charge_3'] }], 'Magic': [{ id: 'magic_mana_kill_1', name: 'Mana Siphon', icon: 'icon_mana_siphon', description: 'Increases mana gained from enemy kills by 10%', maxLevels: 5, costPerLevel: 1, position: { x: 1024, y: 650 }, dependencies: [] }, { id: 'magic_destruction_2', name: 'Path of Destruction', icon: 'icon_path_of_destruction', description: 'Increases damage of Fire Wall and Thunderstorm by 4%', maxLevels: 5, costPerLevel: 1, position: { x: 624, y: 1000 }, dependencies: ['magic_mana_kill_1'] }, { id: 'magic_manipulation_2', name: 'Path of Manipulation', icon: 'icon_path_of_manipulation', description: 'Reduces mana cost of Curse, Summon Golem, and Healing Field by 3%', maxLevels: 5, costPerLevel: 1, position: { x: 1424, y: 1000 }, dependencies: ['magic_mana_kill_1'] }, { id: 'magic_firewall_duration_3', name: 'Eternal Flame', icon: 'icon_eternal_flame', description: 'Increases Fire Wall duration by 10%', maxLevels: 3, costPerLevel: 1, position: { x: 424, y: 1350 }, dependencies: ['magic_destruction_2'] }, { id: 'magic_storm_duration_3', name: 'Lasting Storm', icon: 'icon_lasting_storm', description: 'Increases Thunderstorm duration by 10%', maxLevels: 3, costPerLevel: 1, position: { x: 824, y: 1350 }, dependencies: ['magic_destruction_2'] }, { id: 'magic_curse_3', name: 'Deeper Curse', icon: 'icon_deeper_curse', description: 'Increases the effectiveness of Curse by 5%', maxLevels: 3, costPerLevel: 1, position: { x: 1224, y: 1350 }, dependencies: ['magic_manipulation_2'] }, { id: 'magic_essence_3', name: 'Reinforced Essence', icon: 'icon_reinforced_essence', description: 'Increases Golem HP and Healing Field power by 5%', maxLevels: 3, costPerLevel: 1, position: { x: 1624, y: 1350 }, dependencies: ['magic_manipulation_2'] }, { id: 'magic_regen_4', name: 'Mana Regeneration', icon: 'icon_mana_regeneration', description: 'Regenerate 1 mana per second', maxLevels: 1, costPerLevel: 5, position: { x: 1224, y: 1700 }, dependencies: ['magic_curse_3'] }, { id: 'magic_golem_4', name: 'Golem\'s Agony', icon: 'icon_golems_agony', description: 'Golem gains +1 damage for every 10% of its missing HP', maxLevels: 1, costPerLevel: 3, position: { x: 1624, y: 1700 }, dependencies: ['magic_essence_3'] }, { id: 'magic_freecast_4', name: 'Mana Echo', icon: 'icon_mana_echo', description: 'Grants a 30% chance to cast a spell for free', maxLevels: 1, costPerLevel: 5, position: { x: 824, y: 1700 }, dependencies: ['magic_storm_duration_3'] }], 'Champions': [{ id: 'champions_cost_1', name: 'Champion\'s Discount', icon: 'icon_champions_cost', description: 'Reduces the recruitment cost of your Hero by 10%', maxLevels: 3, costPerLevel: 1, position: { x: 1024, y: 650 }, dependencies: [] }, { id: 'champions_hp_1', name: 'Heroic Vitality', icon: 'icon_champions_hp', description: 'Increases the HP of all Heroes by 5%', maxLevels: 5, costPerLevel: 2, position: { x: 750, y: 950 }, dependencies: ['champions_cost_1'] }, { id: 'champions_dmg_1', name: 'Heroic Strength', icon: 'icon_champions_dmg', description: 'Increases the Damage of all Heroes by 5%', maxLevels: 5, costPerLevel: 2, position: { x: 1300, y: 950 }, dependencies: ['champions_cost_1'] }, { id: 'champions_paladin_block_1', name: 'Divine Shield', icon: 'icon_champions_paladin_block', description: 'Grants the Paladin a 15% chance to block an incoming attack.', maxLevels: 3, costPerLevel: 2, position: { x: 550, y: 1250 }, dependencies: ['champions_hp_1'] }, { id: 'champions_archmage_multicast_1', name: 'Arcane Flurry', icon: 'icon_champions_archmage_multicas', description: 'Grants the Archmage a 10% chance to instantly attack a second time.', maxLevels: 3, costPerLevel: 2, position: { x: 1200, y: 1250 }, dependencies: ['champions_dmg_1'] }, { id: 'champions_ranger_stun_1', name: 'Concussive Shot', icon: 'icon_champions_ranger_stun', description: 'Grants the Ranger Lord\'s attacks a 10% chance to stun the target for 2s.', maxLevels: 3, costPerLevel: 2, position: { x: 1500, y: 1250 }, dependencies: ['champions_dmg_1'] }, { id: 'champions_paladin_aura_power_1', name: 'Aura of Protection', icon: 'icon_champions_paladin_aura', description: 'Increases the effectiveness of the Paladin\'s defensive aura by 20%.', maxLevels: 1, costPerLevel: 4, position: { x: 550, y: 1550 }, dependencies: ['champions_paladin_block_1'] }, { id: 'champions_archmage_burn_1', name: 'Scorched Earth', icon: 'icon_champions_archmage_burn', description: 'The Archmage\'s splash damage has a 30% chance to set all affected enemies on fire.', maxLevels: 1, costPerLevel: 4, position: { x: 1200, y: 1550 }, dependencies: ['champions_archmage_multicast_1'] }, { id: 'champions_ranger_crit_1', name: 'Lethal Precision', icon: 'icon_champions_ranger_crit', description: 'Grants the Ranger Lord a 30% chance to deal double damage.', maxLevels: 1, costPerLevel: 4, position: { x: 1500, y: 1550 }, dependencies: ['champions_ranger_stun_1'] }, { id: 'champions_paladin_max_1', name: 'Unwavering Legion', icon: 'icon_champions_paladin_max', description: 'Increases the maximum number of Paladins you can recruit by 1.', maxLevels: 3, costPerLevel: 5, position: { x: 550, y: 1850 }, dependencies: ['champions_paladin_aura_power_1'] }, { id: 'champions_archmage_max_1', name: 'Echoes of Power', icon: 'icon_champions_archmage_max', description: 'Increases the maximum number of Archmages you can recruit by 1.', maxLevels: 3, costPerLevel: 5, position: { x: 1200, y: 1850 }, dependencies: ['champions_archmage_burn_1'] }, { id: 'champions_ranger_max_1', name: 'Phantom Patrol', icon: 'icon_champions_ranger_max', description: 'Increases the maximum number of Ranger Lords you can recruit by 1.', maxLevels: 3, costPerLevel: 5, position: { x: 1500, y: 1850 }, dependencies: ['champions_ranger_crit_1'] }] }; 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 showWaveNotification(paths, waveNumber) { var message = ""; if (waveNumber >= 10) { if (waveNumber === 10) { message = "Defend All Roads"; } } else { if (paths.length > 0) { var pathNames = paths.map(function (p) { return p.replace('hellgate_', '').charAt(0).toUpperCase() + p.replace('hellgate_', '').slice(1); }).join(' and '); var roadText = paths.length > 1 ? "Roads" : "Road"; message = "Defend " + pathNames + " " + roadText; } } if (message) { var notification = new WaveNotification(message); game.addChild(notification); } } function shakeScreen(intensity, duration) { var initialX = game.x; var initialY = game.y; var shakeTicker = 0; var shakeDuration = Math.ceil(duration / 10); // Czas trwania w cyklach function doShake() { if (shakeTicker >= shakeDuration) { game.x = initialX; game.y = initialY; return; } var shakeX = (Math.random() - 0.5) * intensity; var shakeY = (Math.random() - 0.5) * intensity; game.x = initialX + shakeX; game.y = initialY + shakeY; shakeTicker++; LK.setTimeout(doShake, 10); } doShake(); } 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 createAuraDamageSparks(x, y) { var numSparks = 3; for (var i = 0; i < numSparks; i++) { var spark = new Container(); var sparkGraphics = spark.attachAsset('star_particle', { anchorX: 0.5, anchorY: 0.5, tint: 0x9400D3 }); spark.x = x; spark.y = y; gateContainer.addChild(spark); var angle = Math.random() * Math.PI * 2; var distance = Math.random() * 20 + 10; tween(spark, { x: x + Math.cos(angle) * distance, y: y + Math.sin(angle) * distance, alpha: 0, scaleX: 0.2, scaleY: 0.2 }, { duration: 400, easing: tween.easeOut, onFinish: function onFinish() { spark.destroy(); } }); } } function applyActiveWeatherModifiers() { if (activeWeatherEvent === 'RAIN') { if (rainIntervalId) { LK.clearInterval(rainIntervalId); } rainIntervalId = LK.setInterval(function () { for (var i = 0; i < 3; i++) { var drop = new Raindrop(); weatherEffectLayer.addChild(drop); } }, 150); } if (activeWeatherEvent === 'FOG') { towers.forEach(function (tower) { if (tower.towerType === 'mage') { tower.addBuff({ source: 'FOG_DEBUFF', effect: { damage: 0.90 } }); } }); if (fogIntervalId) { LK.clearInterval(fogIntervalId); } // Uruchamiamy "fabrykę" mgły, która dba o stałą jej ilość fogIntervalId = LK.setInterval(function () { if (activeFogWisps.length < 6) { // Utrzymujemy 6 obiektów mgły na ekranie var wisp = new FogWisp(); weatherEffectLayer.addChild(wisp); activeFogWisps.push(wisp); } }, 1000); // Co sekundę sprawdzaj i dodawaj } if (activeWeatherEvent === 'NIGHT') { nightOverlay.visible = true; tween(nightOverlay, { alpha: 0.6 }, { duration: 500 }); } else if (activeWeatherEvent === 'STORM') { nightOverlay.visible = true; tween(nightOverlay, { alpha: 0.4 }, { duration: 500 }); if (stormIntervalId) { LK.clearInterval(stormIntervalId); } stormIntervalId = LK.setInterval(function () { var strikeX = Math.random() * 2048; var strikeY = Math.random() * 2732; var strike = new LightningStrikeEffect(strikeX, strikeY); spellEffectLayer.addChild(strike); shakeScreen(8, 150); }, 2500); if (rainIntervalId) { LK.clearInterval(rainIntervalId); } rainIntervalId = LK.setInterval(function () { for (var i = 0; i < 3; i++) { var drop = new Raindrop(); weatherEffectLayer.addChild(drop); } }, 150); } } function clearActiveWeatherModifiers() { if (rainIntervalId) { LK.clearInterval(rainIntervalId); rainIntervalId = null; } if (fogIntervalId) { LK.clearInterval(fogIntervalId); fogIntervalId = null; } if (stormIntervalId) { LK.clearInterval(stormIntervalId); stormIntervalId = null; } for (var i = activeFogWisps.length - 1; i >= 0; i--) { activeFogWisps[i].destroy(); } activeFogWisps = []; if (nightOverlay.visible) { tween(nightOverlay, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { nightOverlay.visible = false; } }); } towers.forEach(function (tower) { tower.removeBuff('FOG_DEBUFF'); }); } 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: 35, damage: 6, fireRate: 60, range: 5 * CELL_SIZE, bulletSpeed: 7, graphic: 'arrow_guard' }, 2: { name: 'Veteran Guard', cost: 70, // Zwiększono z 60 damage: 9, fireRate: 70, multiShot: 2, range: 5 * CELL_SIZE, bulletSpeed: 7, graphic: 'arrow_guard' }, 3: { name: 'Elite Guard', cost: 155, // Zwiększono ze 120 damage: 14, fireRate: 85, multiShot: 3, range: 6 * CELL_SIZE, bulletSpeed: 7, graphic: 'arrow_guard' } }, 'crossbow': { 1: { name: 'Crossbow Tower', cost: 45, damage: 12, fireRate: 150, range: 7 * CELL_SIZE, bulletSpeed: 15, graphic: 'arrow_crossbow' }, 2: { name: 'Heavy Crossbow', cost: 80, // Zwiększono ze 100 damage: 16, fireRate: 150, range: 7.5 * CELL_SIZE, bulletSpeed: 15, graphic: 'arrow_crossbow' }, 3: { name: 'Arbalest', cost: 200, // Zwiększono z 200 damage: 23, fireRate: 150, multiShot: 2, range: 8 * CELL_SIZE, bulletSpeed: 15, graphic: 'arrow_crossbow' } }, 'mage': { 1: { name: 'Mage Tower', cost: 45, damage: 6, fireRate: 90, range: 5 * CELL_SIZE, bulletSpeed: 8, bulletType: 'normal', graphic: 'bullet' }, '2F': { name: 'Fire Mage', cost: 100, damage: 10, fireRate: 120, range: 6 * CELL_SIZE, bulletSpeed: 8, bulletType: 'splash', splashDamage: 8, splashRadius: 3.0 * CELL_SIZE, splashScale: 2.0, graphic: 'projectile_fireball' }, '3F': { name: 'Pyromancer', cost: 200, damage: 15, fireRate: 120, range: 6.5 * CELL_SIZE, bulletSpeed: 8, bulletType: 'splash', splashDamage: 12, splashRadius: 3.8 * CELL_SIZE, splashScale: 2.5, graphic: 'projectile_fireball' }, '2W': { name: 'Water Mage', cost: 100, damage: 7, fireRate: 140, range: 6 * CELL_SIZE, bulletSpeed: 8, bulletType: 'slow', slowAmount: 0.6, multiShot: 2, slowDuration: 180, slowChance: 0.4, graphic: ['ice_spike_1', 'ice_spike_2', 'ice_spike_3', 'ice_spike_4', 'ice_spike_5'] }, '3W': { name: 'Ice Archmage', cost: 210, damage: 12, fireRate: 160, range: 6.5 * CELL_SIZE, bulletSpeed: 8, bulletType: 'slow', slowAmount: 0.5, multiShot: 2, slowDuration: 220, slowChance: 0.6, graphic: ['ice_spike_1', 'ice_spike_2', 'ice_spike_3', 'ice_spike_4', 'ice_spike_5'] }, '2L': { name: 'Lightning Mage', cost: 100, damage: 9, fireRate: 150, range: 7 * CELL_SIZE, bulletSpeed: 20, bulletType: 'chain', chainTargets: 3, chainDamageFalloff: 1, chainChance: 0.25, stunChance: 0.15, chainStunChance: 0.10, stunDuration: 140, graphic: 'bullet' }, '3L': { name: 'Stormcaller', cost: 210, damage: 16, fireRate: 150, range: 7.5 * CELL_SIZE, bulletSpeed: 20, bulletType: 'chain', chainTargets: 4, chainDamageFalloff: 1, chainChance: 0.45, stunChance: 0.15, chainStunChance: 0.10, stunDuration: 90, graphic: 'bullet' } }, 'banner': { 1: { name: 'War Banner', cost: 40, damage: 0, range: 4 * CELL_SIZE, description: 'Increases attack speed of nearby towers by 15%.', aura: { 'attackSpeed': 1.15 } }, '2A': { name: 'Banner of Fury', cost: 130, range: 4 * CELL_SIZE, description: 'Greatly increases attack speed of nearby towers by 30%.', aura: { 'attackSpeed': 1.3 } }, '2B': { name: 'Banner of Command', cost: 130, range: 4 * CELL_SIZE, description: 'Increases attack speed and damage of nearby towers by 15%.', aura: { 'attackSpeed': 1.15, 'damage': 1.15 } } } }; var BUILDING_DATA = { 'home': { id: 'home', name: 'Home', size: { w: 3, h: 3 }, cost: 35, description: 'Each Home increases your max Population by 5.' }, 'training_ground': { id: 'training_ground', name: 'Training Ground', size: { w: 3, h: 3 }, // Przykładowy rozmiar cost: 100, // Przykładowy koszt description: 'All recruited units gain +10% Health and Attack. This effect stacks.' }, 'altar_of_champions': { id: 'altar_of_champions', name: 'Altar', size: { w: 4, h: 4 }, cost: 250, description: 'A monumental structure that calls upon legendary heroes. Allows you to choose and recruit one of three powerful champions for your army.' }, 'workshop': { id: 'workshop', name: 'Workshop', size: { w: 4, h: 3 }, cost: 150, description: 'Unlocks advanced tower upgrades (Level 3).' }, 'magic_sanctum': { id: 'magic_sanctum', name: 'Magic Sanctum', size: { w: 3, h: 3 }, // Przykładowy rozmiar cost: 100, // Przykładowy koszt description: 'Increases the effectiveness of all spells by 10%. This effect stacks.' }, 'barracks': { id: 'barracks', name: 'Barracks', size: { w: 6, h: 7 }, cost: 150, description: 'Unlocks the Army panel and allows recruitment of units.' }, 'magic_academy': { id: 'magic_academy', name: 'Magic Academy', size: { w: 6, h: 8 }, cost: 150, description: 'Unlocks Mana and the Magic panel, allowing you to cast powerful spells.' }, 'tavern': { id: 'tavern', name: 'Tavern', size: { w: 5, h: 5 }, cost: 100, 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: 3 }, cost: 150, description: 'Provides global buffs to towers and units.' }, 'infirmary': { id: 'infirmary', name: 'Infirmary', size: { w: 4, h: 4 }, cost: 150, 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: 35, radius: 2.5 * CELL_SIZE, duration: 460, dotDamage: 9, dotDuration: 230 }, 'healing_field': { id: 'healing_field', name: 'Healing Field', description: 'Heals allied targets in an area.', cost: 30, heal: 25, radius: 3 * CELL_SIZE }, 'lightning_stun': { id: 'lightning_stun', name: 'Thunderstorm', description: 'Stuns and damages enemies in an area.', cost: 35, stunDuration: 2 * 60, radius: 3 * CELL_SIZE, damage: 19, duration: 110 }, 'summon_golem': { id: 'summon_golem', name: 'Summon Golem', description: 'Summons a temporary blocker unit.', cost: 40, radius: 0 }, 'curse': { id: 'curse', name: 'Curse', description: 'Cursed enemies move slower and take more damage.', cost: 35, damageMultiplier: 1.10, slowFactor: 0.85, duration: 480, radius: 3.5 * CELL_SIZE } }; var LEVEL_DATA_ALL = { 1: { levelName: "Beginner Path", mapAsset: 'map1', initialGold: 700, initialLives: 15, gridStartX: 140, gridStartY: 200 - CELL_SIZE * 4, mapLayout: ["222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "2222222211000AA222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "22222222CC00011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "2222222211000AA222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "222222221100011222222222", "22222222CC00011222222222", "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: 11, startY: 5, goalX: 12, goalY: 34, friendlySpawnOffset: 0, lanes: [{ id: 'mainPath_L', offset: -0.6 }, { id: 'mainPath_C', offset: 0 }, { id: 'mainPath_R', offset: 0.6 }] }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 140, units: [{ spawner: 'mainPath', type: 'orc_normal', count: 12 }] }] }, { wave: 2, phases: [{ delay: 0, interval: 100, units: [{ spawner: 'mainPath', type: 'orc_normal', count: 10 }] }, { delay: 450, interval: 120, units: [{ spawner: 'mainPath', type: 'orc_shield', count: 6 }] }] }, { wave: 3, phases: [{ delay: 0, interval: 110, units: [{ spawner: 'mainPath', type: 'orc_normal', count: 8 }, { spawner: 'mainPath', type: 'orc_shield', count: 3 }] }, { delay: 600, interval: 60, units: [{ spawner: 'mainPath', type: 'orc_rider', count: 9 }] }] }, { wave: 4, phases: [{ delay: 0, interval: 130, units: [{ spawner: 'mainPath', type: 'orc_shield', count: 12 }, { spawner: 'mainPath', type: 'orc_archer', count: 10 }] }] }, { wave: 5, phases: [{ delay: 0, interval: 70, units: [{ spawner: 'mainPath', type: 'orc_normal', count: 15 }] }, { delay: 400, interval: 110, units: [{ spawner: 'mainPath', type: 'orc_shield', count: 10 }, { spawner: 'mainPath', type: 'orc_archer', count: 8 }] }, { delay: 400, interval: 60, units: [{ spawner: 'mainPath', type: 'orc_rider', count: 10 }] }] }, { wave: 6, phases: [{ delay: 0, interval: 100, units: [{ spawner: 'mainPath', type: 'orc_chieftain', count: 1, isBoss: true }, { spawner: 'mainPath', type: 'orc_shield', count: 10 }, { spawner: 'mainPath', type: 'orc_archer', count: 8 }] }] }] }, 2: { levelName: "Old Mill Path", mapAsset: 'map2', initialGold: 900, initialLives: 15, gridStartX: 140, gridStartY: 200 - CELL_SIZE * 4, mapLayout: ["110021111222222222222222", "110021111222222222222222", "110021111222222222222222", "110021111222222222222222", "110021111222222222222222", "110021111222222222222222", "AA0021111222222222222222", "AA0021111111111111111111", "110021111111111111111111", "11002AA11111111111111111", "11002AA11111111111111111", "110000000000000000000000", "110000000000000000000000", "222222222222222222222200", "111111111CC1111111111100", "111111111CC1111111111100", "111111111111111111111100", "111111111111111111111100", "222222222222222221111100", "22222222222222222111CC00", "22222222222222222111CC00", "222222222222222221111100", "222222222222222221111100", "222222222222222221111100", "222222222222222221111100", "222222222222222221111100", "222222222222222221111100", "222222222222222221111100", "222222222222222221111E00"], 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: 2, startY: 5, goalX: 22, goalY: 33, friendlySpawnOffset: 0, lanes: [{ id: 'mainPath_L', offset: 0.2 }, { id: 'mainPath_C', offset: 0 }, { id: 'mainPath_R', offset: 1.0 }] }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 120, units: [{ spawner: 'mainPath', type: 'orc_normal', count: 15 }, { spawner: 'mainPath', type: 'orc_rider', count: 6 }] }] }, { wave: 2, phases: [{ delay: 0, interval: 150, units: [{ spawner: 'mainPath', type: 'orc_shield', count: 18 }] }, { delay: 600, interval: 100, units: [{ spawner: 'mainPath', type: 'orc_archer', count: 12 }] }] }, { wave: 3, phases: [{ delay: 0, interval: 80, units: [{ spawner: 'mainPath', type: 'orc_normal', count: 30 }] }, { delay: 500, interval: 50, units: [{ spawner: 'mainPath', type: 'orc_rider', count: 15 }] }] }, { wave: 4, phases: [{ delay: 0, interval: 120, units: [{ spawner: 'mainPath', type: 'orc_rider', count: 20 }, { spawner: 'mainPath', type: 'orc_shield', count: 10 }] }] }, { wave: 5, phases: [{ delay: 0, interval: 100, units: [{ spawner: 'mainPath', type: 'orc_chieftain', count: 1, isBoss: true }, { spawner: 'mainPath', type: 'orc_shield', count: 20 }, { spawner: 'mainPath', type: 'orc_normal', count: 20 }, { spawner: 'mainPath', type: 'orc_archer', count: 18 }] }] }, { wave: 6, phases: [{ delay: 0, interval: 80, units: [{ spawner: 'mainPath', type: 'orc_chieftain', count: 2, isBoss: true }, { spawner: 'mainPath', type: 'orc_rider', count: 18 }, { spawner: 'mainPath', type: 'orc_archer', count: 18 }] }] }] }, 3: { levelName: "Twin Pass", mapAsset: 'map3', initialGold: 1200, initialLives: 15, gridStartX: 235, gridStartY: 200 - CELL_SIZE * 4, mapLayout: ["111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000AA1100000111111", "111100000AA1100000111111", "11110000011CC00000111111", "11110000011CC00000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000AA1100000111111", "111100000AA1100000111111", "111100000111100000111111", "11110000011CC00000111111", "11110000011CC00000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100000111100000111111", "111100E00111100E00111111"], 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: 'leftPath', startX: 6, startY: 5, goalX: 6, goalY: 34, friendlySpawnOffset: 0.2, lanes: [{ id: 'leftPath_L', offset: -1 }, { id: 'leftPath_R', offset: 1 }] }, { id: 'rightPath', startX: 15, startY: 5, goalX: 15, goalY: 34, friendlySpawnOffset: 0, lanes: [{ id: 'rightPath_L', offset: -1 }, { id: 'rightPath_R', offset: 1 }] }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 140, units: [{ spawner: 'leftPath', type: 'orc_normal', count: 10 }] }, { delay: 0, interval: 140, units: [{ spawner: 'rightPath', type: 'orc_normal', count: 10 }] }] }, { wave: 2, phases: [{ delay: 0, interval: 120, units: [{ spawner: 'leftPath', type: 'orc_shield', count: 8 }] }, { delay: 0, interval: 120, units: [{ spawner: 'rightPath', type: 'orc_rider', count: 12 }] }] }, { wave: 3, phases: [{ delay: 0, interval: 100, units: [{ spawner: 'leftPath', type: 'orc_rider', count: 15 }] }, { delay: 0, interval: 100, units: [{ spawner: 'rightPath', type: 'orc_normal', count: 20 }] }] }, { wave: 4, phases: [{ delay: 0, interval: 130, units: [{ spawner: 'leftPath', type: 'orc_shield', count: 12 }, { spawner: 'leftPath', type: 'orc_archer', count: 10 }] }, { delay: 0, interval: 130, units: [{ spawner: 'rightPath', type: 'orc_shield', count: 12 }, { spawner: 'rightPath', type: 'orc_archer', count: 10 }] }] }, { wave: 5, phases: [{ delay: 0, interval: 90, units: [{ spawner: 'leftPath', type: 'orc_normal', count: 35 }] }, { delay: 300, interval: 90, units: [{ spawner: 'rightPath', type: 'orc_rider', count: 15 }, { spawner: 'rightPath', type: 'orc_archer', count: 10 }] }] }, { wave: 6, phases: [{ delay: 0, interval: 120, units: [{ spawner: 'leftPath', type: 'orc_chieftain', count: 1, isBoss: true }, { spawner: 'leftPath', type: 'orc_shield', count: 15 }] }, { delay: 0, interval: 120, units: [{ spawner: 'rightPath', type: 'orc_chieftain', count: 1, isBoss: true }, { spawner: 'rightPath', type: 'orc_archer', count: 20 }] }] }] }, 4: { levelName: "Winter Is Coming", mapAsset: 'map5', initialGold: 1200, initialLives: 15, gridStartX: 160, gridStartY: 250 - CELL_SIZE * 4, mapLayout: ["22222222222222222221112000", "22222222222222222221112000", "22222222222222222221112000", "22222221111111111111110000", "22222221111111111111112000", "22222221CC1111111111112000", "22222111110000000000000000", "22222111110000000000000000", "22222111110002222222222222", "22222111110001111111111111", "22222111110001111111111111", "22222111110001111222222222", "2222211111000CC11222222222", "22222111110001111222222222", "22222111110001111222222222", "22222111110001111222222222", "22222111110001111222222222", "2222211111000AA11222222222", "11111111110001111222222222", "11111111110001111222222222", "1111111AA10001111222222222", "00000000000001111222222222", "00000000000001111222222222", "00000000000001111222222222", "00211111111111111222222222", "00211111111111111222222222", "00211111222222222222222222", "0022211111222222222222222222", "E022222222222222222222222222"], 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: 23, startY: 5, goalX: 1, goalY: 32, friendlySpawnOffset: -1, lanes: [{ id: 'mainPath_L', offset: -0.7 }, { id: 'mainPath_C', offset: -0.3 }, { id: 'mainPath_R', offset: 0.1 }] }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 120, units: [{ spawner: 'mainPath', type: 'undead_soldier', count: 6 }, { spawner: 'mainPath', type: 'undead_archer', count: 6 }, { spawner: 'mainPath', type: 'undead_soldier', count: 6 }] }] }, { wave: 2, phases: [{ delay: 0, interval: 90, units: [{ spawner: 'mainPath', type: 'undead_shielder', count: 6 }, { spawner: 'mainPath', type: 'undead_soldier', count: 6 }, { spawner: 'mainPath', type: 'undead_shielder', count: 6 }, { spawner: 'mainPath', type: 'undead_archer', count: 6 }] }] }, { wave: 3, phases: [{ delay: 0, interval: 75, units: [{ spawner: 'mainPath', type: 'undead_soldier', count: 10 }, { spawner: 'mainPath', type: 'undead_shielder', count: 8 }, { spawner: 'mainPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'mainPath', type: 'undead_archer', count: 12 }] }] }, { wave: 4, phases: [{ delay: 0, interval: 70, units: [{ spawner: 'mainPath', type: 'undead_shielder', count: 8 }, { spawner: 'mainPath', type: 'undead_horseman', count: 6 }, { spawner: 'mainPath', type: 'death_knight', count: 1 }, { spawner: 'mainPath', type: 'undead_soldier', count: 10 }, { spawner: 'mainPath', type: 'undead_archer', count: 12 }, { spawner: 'mainPath', type: 'death_knight', count: 1 }] }] }, { wave: 5, phases: [{ delay: 0, interval: 60, units: [{ spawner: 'mainPath', type: 'undead_horseman', count: 12 }, { spawner: 'mainPath', type: 'undead_archer', count: 10 }, { spawner: 'mainPath', type: 'death_knight', count: 1 }, { spawner: 'mainPath', type: 'undead_horseman', count: 10 }, { spawner: 'mainPath', type: 'undead_archer', count: 10 }, { spawner: 'mainPath', type: 'death_knight', count: 1 }] }] }, { wave: 6, phases: [{ delay: 0, interval: 80, units: [{ spawner: 'mainPath', type: 'undead_shielder', count: 20 }, { spawner: 'mainPath', type: 'death_knight', count: 2 }, { spawner: 'mainPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'mainPath', type: 'undead_archer', count: 20 }] }] }] }, 5: { levelName: "Frozen Twins", mapAsset: 'map4', initialGold: 1600, initialLives: 15, gridStartX: 100, gridStartY: 200 - CELL_SIZE * 4, mapLayout: ["1100001111222221111000011", "1100001111222221111000011", "110000CC11222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "11000011112222211AA000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "110000AA11222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "11000011112222211CC000011", "1100001111222221111000011", "1100001111222221111000011", "1100001111222221111000011", "11EEEEE11112222111EEEEE11", "1122222111122221112222211"], 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: 'leftPath', startX: 3, startY: 5, goalX: 3, goalY: 33, friendlySpawnOffset: 0, lanes: [{ id: 'leftPath_L', offset: -0.5 }, { id: 'leftPath_R', offset: 0.5 }] }, { id: 'rightPath', startX: 21, startY: 5, goalX: 22, goalY: 33, friendlySpawnOffset: 0, lanes: [{ id: 'rightPath_L', offset: -0.5 }, { id: 'rightPath_R', offset: 0.5 }] }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 150, units: [{ spawner: 'leftPath', type: 'undead_soldier', count: 15 }, { spawner: 'rightPath', type: 'undead_soldier', count: 15 }] }] }, { wave: 2, phases: [{ delay: 0, interval: 130, units: [{ spawner: 'leftPath', type: 'undead_shielder', count: 10 }, { spawner: 'leftPath', type: 'undead_archer', count: 8 }] }, { delay: 250, interval: 110, units: [{ spawner: 'rightPath', type: 'undead_soldier', count: 12 }, { spawner: 'rightPath', type: 'undead_shielder', count: 10 }] }] }, { wave: 3, phases: [{ delay: 0, interval: 90, units: [{ spawner: 'leftPath', type: 'undead_soldier', count: 12 }, { spawner: 'leftPath', type: 'undead_shielder', count: 10 }, { spawner: 'leftPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'leftPath', type: 'undead_archer', count: 12 }] }, { delay: 200, interval: 70, units: [{ spawner: 'rightPath', type: 'undead_horseman', count: 18 }] }] }, { wave: 4, phases: [{ delay: 0, interval: 80, units: [{ spawner: 'leftPath', type: 'undead_shielder', count: 18 }, { spawner: 'leftPath', type: 'death_knight', count: 2 }] }, { delay: 300, interval: 80, units: [{ spawner: 'rightPath', type: 'undead_horseman', count: 18 }, { spawner: 'rightPath', type: 'death_knight', count: 2 }] }] }, { wave: 5, phases: [{ delay: 0, interval: 75, units: [{ spawner: 'leftPath', type: 'undead_archer', count: 12 }, { spawner: 'leftPath', type: 'undead_shielder', count: 12 }, { spawner: 'leftPath', type: 'death_knight', count: 2 }, { spawner: 'rightPath', type: 'undead_horseman', count: 12 }, { spawner: 'rightPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'rightPath', type: 'undead_archer', count: 12 }] }] }, { wave: 6, phases: [{ delay: 0, interval: 80, units: [{ spawner: 'leftPath', type: 'undead_shielder', count: 24 }, { spawner: 'leftPath', type: 'death_knight', count: 3 }] }, { delay: 500, interval: 80, units: [{ spawner: 'rightPath', type: 'undead_horseman', count: 24 }, { spawner: 'rightPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'rightPath', type: 'undead_archer', count: 20 }] }] }] }, 6: { levelName: "Titan's Fork", mapAsset: 'map6', initialGold: 1100, initialLives: 15, gridStartX: 240, gridStartY: 240 - CELL_SIZE * 4, mapLayout: ["222112000111100011222222", "222112000111100011222222", "222112000111100011222222", "2221120001CC100011222222", "222112000111100011222222", "222112000111100011222222", "222112000111100011222222", "222112000111100011222222", "2221120001AA100011222222", "222112000111100011222222", "222112000222200011222222", "222112000222200011222222", "222112000222200011222222", "222112000222200011222222", "222112000222200011222222", "222112000000000011222222", "222112000000000011222222", "222112222200222211222222", "222221111200211112222222", "2222211CC2002AA112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221111200211112222222", "222221122200221112222222", "2222222222EE222222222222"], 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: 'leftPath', startX: 6, startY: 5, goalX: 11, goalY: 31, lanes: [{ id: 'leftPath_C', offset: 0 }] }, { id: 'rightPath', startX: 14, startY: 5, goalX: 10, goalY: 31, lanes: [{ id: 'rightPath_C', offset: 0 }] }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 140, units: [{ spawner: 'leftPath', type: 'undead_soldier', count: 18 }, { spawner: 'leftPath', type: 'undead_shielder', count: 10 }, { spawner: 'rightPath', type: 'undead_soldier', count: 18 }, { spawner: 'rightPath', type: 'undead_shielder', count: 10 }] }] }, { wave: 2, phases: [{ delay: 0, interval: 120, units: [{ spawner: 'leftPath', type: 'undead_shielder', count: 12 }, { spawner: 'leftPath', type: 'death_knight', count: 1 }, { spawner: 'leftPath', type: 'undead_shielder', count: 12 }, { spawner: 'rightPath', type: 'undead_archer', count: 12 }, { spawner: 'rightPath', type: 'death_knight', count: 1 }, { spawner: 'rightPath', type: 'undead_archer', count: 12 }] }] }, { wave: 3, phases: [{ delay: 0, interval: 85, units: [{ spawner: 'leftPath', type: 'undead_soldier', count: 18 }, { spawner: 'leftPath', type: 'undead_shielder', count: 15 }, { spawner: 'leftPath', type: 'death_knight', count: 2 }, { spawner: 'rightPath', type: 'undead_archer', count: 18 }, { spawner: 'rightPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'rightPath', type: 'undead_archer', count: 12 }] }] }, { wave: 4, phases: [{ delay: 0, interval: 70, units: [{ spawner: 'leftPath', type: 'undead_horseman', count: 12 }, { spawner: 'leftPath', type: 'death_knight', count: 2 }, { spawner: 'leftPath', type: 'undead_horseman', count: 12 }, { spawner: 'rightPath', type: 'undead_shielder', count: 12 }, { spawner: 'rightPath', type: 'death_knight', count: 2 }, { spawner: 'rightPath', type: 'undead_shielder', count: 12 }] }] }, { wave: 5, phases: [{ delay: 0, interval: 80, units: [{ spawner: 'leftPath', type: 'undead_horseman', count: 30 }, { spawner: 'rightPath', type: 'undead_shielder', count: 20 }, { spawner: 'rightPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'rightPath', type: 'death_knight', count: 4 }] }] }, { wave: 6, phases: [{ delay: 0, interval: 80, units: [{ spawner: 'leftPath', type: 'death_knight', count: 3 }, { spawner: 'leftPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'leftPath', type: 'undead_soldier', count: 24 }, { spawner: 'rightPath', type: 'death_knight', count: 3 }, { spawner: 'rightPath', type: 'lich', count: 1, isBoss: true }, { spawner: 'rightPath', type: 'undead_archer', count: 24 }] }] }] }, 7: { levelName: "", mapAsset: 'map7', initialGold: 900, initialLives: 20, gridStartX: 46, gridStartY: 200 - CELL_SIZE * 4, mapLayout: ["222000111111000111111000222", "222000111111000111111000222", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "2AA000111111000111111000CC1", "211000111111000111111000111", "211000111111000111111000111", "211000111AA10001CC111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "2110001CC111000111AA1000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "2110001CC1110001AA111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "211000111111000111111000111", "222000111111000111111000222", "222E00111111000111111000222"], 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: 'hellgate_left', startX: 4, startY: 5, goalX: 4, goalY: 33, lanes: [{ id: 'hellgate_left_C', offset: 0 }] }, { id: 'hellgate_center', startX: 12, startY: 5, goalX: 12, goalY: 33, lanes: [{ id: 'hellgate_center_C', offset: 0 }] }, { id: 'hellgate_right', startX: 22, startY: 5, goalX: 22, goalY: 33, lanes: [{ id: 'hellgate_right_C', offset: 0 }] }], waves: [{ wave: 1, phases: [{ delay: 0, interval: 100, units: [{ type: 'demon_soldier' }, { type: 'demon_archer' }, { type: 'demon_defender' }, { type: 'demon_rider' }, { type: 'demon_lord' }, { type: 'demon_tamer' }] }] }] } }; var EVENT_DATA = { 'SUNNY': { name: 'Sunny Day', description: 'A calm day. No special effects.', asset: 'card_sunny' }, 'RAIN': { name: 'Rainy Day', description: 'Enemies drop 20% less gold.', asset: 'card_rain' }, 'NIGHT': { name: 'Night', description: 'Enemies are 10% stronger.', asset: 'card_night', modifier: { enemyStatMultiplier: 1.10 } }, 'STORM': { name: 'Storm', description: 'Recruitment of all units is disabled for this wave.', asset: 'card_storm' }, 'FOG': { name: 'Fog', description: 'Magic is disabled.\nMage Towers deal 10% less damage.', asset: 'card_fog', modifier: { mageTowerDebuff: 0.90 } } }; var WEATHER_DECK_DATA = { 1: ['SUNNY', 'RAIN', 'SUNNY', 'NIGHT', 'SUNNY'], 2: ['SUNNY', 'RAIN', 'NIGHT', 'SUNNY', 'FOG'], 3: ['SUNNY', 'RAIN', 'NIGHT', 'FOG', 'STORM'], 4: ['SUNNY', 'RAIN', 'NIGHT', 'FOG', 'STORM'], 5: ['SUNNY', 'RAIN', 'NIGHT', 'FOG', 'STORM'], 6: ['SUNNY', 'RAIN', 'NIGHT', 'FOG', 'STORM'] }; var GUIDE_DATA = [{ title: "Welcome to the Kingdom!", image: "guide_image_welcome", text: "This guide will teach you the basics of defending your castle." }, { title: "Earning Skill Points", image: "guide_image_skill_points", text: "Skill points are the key to power. You earn them by surviving waves. Levels 1-3 have a 15-point cap each, while Levels 4-6 have a 12-point cap. You can earn a maximum of 81 skill points in total." }, { title: "Building Your Defenses", image: "guide_image_towers", text: "Towers are your main line of defense, and you should focus on them at the beginning of the game. You can build them by dragging them from the bottom panel onto available tiles on the map. Build them at any time to strengthen your position!" }, { title: "Commanding Your Army", image: "guide_image_army", text: "To command an army, you must first build the Barracks in your castle. Once unlocked, a strong army can turn the tide of battle. Recruit units from the Army panel, and they will automatically spawn near your castle to engage the enemy." }, { title: "Mastering the Arcane", image: "guide_image_magic", text: "To wield powerful magic, you must first construct the Magic Academy in your castle. This unlocks your Mana resource and the Magic panel. Mana is gained from defeating enemies and can be enhanced with skills. Cast spells by dragging them from the panel onto the battlefield." }, { title: "Commanding Heroes", image: "guide_image_heroes", text: "To summon a powerful Hero, you must first build the 'Altar' in your castle. Once constructed, you will be presented with a choice of three unique champions. Choose wisely, as your decision is final for that battle! Your chosen hero can then be recruited from the Army panel." }, { title: "The Skill Tree", image: "guide_image_skills", text: "You earn skill points by surviving waves on any level. The further you get, the more points you earn! Once you've earned all points from a level, play other levels to keep progressing. Use the 'Reset' button at any time to get your points back and try a new build!" }, { title: "The Castle View", image: "guide_image_castle", text: "Between waves, you can switch to the Castle View. Here you can build special buildings that provide global bonuses, like increased income from Taverns or stronger units from the Smithy.\n\nP.S. Don't click on your citizens... they don't like it!" }]; var UNIT_DATA = { 'swordsman': { id: 'swordsman', name: 'Swordsman', cost: 20, speed: 1.2, maxHealth: 20, damage: 3, attackSpeed: 90, range: 1.2 * CELL_SIZE, detectionRange: 3 * CELL_SIZE, isRanged: false }, 'defender': { id: 'defender', name: 'Defender', cost: 30, 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: 50, 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: 30, speed: 1.1, maxHealth: 15, damage: 6, attackSpeed: 48, range: 5 * CELL_SIZE, detectionRange: 6 * CELL_SIZE, isRanged: true } }; var HERO_DATA = { 'paladin': { id: 'paladin', name: 'Paladin', image: 'unit_paladin', cost: 300, speed: 0.9, maxHealth: 300, damage: 20, attackSpeed: 120, range: 1.5 * CELL_SIZE, detectionRange: 3.6 * CELL_SIZE, isRanged: false, special: 'Aura of Defense: Grants nearby allied units a defense bonus, reducing incoming damage.' }, 'archmage': { id: 'archmage', name: 'Archmage', image: 'unit_archmage', cost: 300, speed: 1.0, maxHealth: 180, damage: 25, attackSpeed: 150, range: 6 * CELL_SIZE, detectionRange: 7 * CELL_SIZE, isRanged: true, special: 'Explosive Spell & Arcane Presence: Attacks deal splash damage. Reduces the mana cost of all player spells by 30% while on the field.' }, 'ranger_lord': { id: 'ranger_lord', name: 'Ranger', image: 'unit_ranger_lord', cost: 300, speed: 1.2, maxHealth: 180, damage: 25, attackSpeed: 200, range: 8 * CELL_SIZE, detectionRange: 10 * CELL_SIZE, isRanged: true, special: 'Penta Shot: Attacks 5 targets simultaneously, dealing damage and slowing them.' } }; var UNITSPEDIA_DATA = { 'Humans': [{ id: 'swordsman', name: 'Swordsman', image: 'unit_swordsman', description: "A basic infantry unit. Cheap and versatile, perfect for creating a frontline and soaking up damage.\n\nYour standard-issue recruit. Give him a sword, point him at the enemy, and he'll spend the evening complaining about his salary at the tavern. Still, he holds the line surprisingly well." }, { id: 'defender', name: 'Defender', image: 'unit_defender', description: "A heavily armored tank. His main job is to block and slow down the strongest enemies, giving your damage dealers time to act.\n\nA walking tin can. Tougher than last week's bread but moves with the grace of a glacier. The enemy is more likely to die of boredom than to get around him." }, { id: 'horseman', name: 'Horseman', image: 'unit_horseman', description: "A fast flanking unit. Can quickly reach enemy archers and disrupt the enemy's formation.\n\nThe fastest pizza deli... that is, cavalry in the kingdom. His horse is smarter than he is, but together they make a deadly duo." }, { id: 'archer', name: 'Archer', image: 'unit_archer', description: "A ranged damage dealer. Attacks from a safe distance, inflicting high damage to single targets. Must be protected from melee combat.\n\nA specialist in maintaining a healthy social distance. Claims he can hit an orc's left eye from 200 yards, but no one's ever had the time to check." }, { id: 'paladin', name: 'Hero: Paladin', image: 'unit_paladin', description: "A legendary champion of light, encased in holy armor. The Paladin is a bastion of defense, capable of holding the line against the darkest of foes while inspiring his allies.\n\nAlways the first to fight and the last to the tavern. Rumor has it his armor doesn't even have a single scratch... because he polishes it every evening." }, { id: 'archmage', name: 'Hero: Archmage', image: 'unit_archmage', description: "A master of the arcane arts who has transcended the limits of mortal magic. The Archmage commands devastating elemental power from a distance.\n\nHe has seen empires fall. Sometimes he forgets why he was casting a spell halfway through, but the effect is usually... explosive anyway." }, { id: 'ranger_lord', name: 'Hero: Ranger Lord', image: 'unit_ranger_lord', description: "An unrivaled markswoman and a master of the wilderness. The Ranger Lord can track any prey and unleash a hail of arrows upon her enemies.\n\nShe talks more to trees and squirrels than to people. Her arrows always hit their mark, mainly because the target never knows the Ranger has been watching them for three days from the top of an oak tree." }], 'Orcs': [{ id: 'orc_normal', name: 'Orc Grunt', image: 'orc_normal', description: "The backbone of the Orc forces. Weak alone, but can be dangerous in large numbers. Classic cannon fodder.\n\nStandard-issue green trouble. He has two simple orders: walk forward and swing his axe. He's surprisingly good at the first part." }, { id: 'orc_shield', name: 'Orc Shieldbearer', image: 'orc_shield', description: "A durable frontline unit. His shield gives him higher survivability, making him effective at protecting more valuable Orcs behind him.\n\nHe was told the big wooden plank he's carrying would make him invincible. He almost believes it. Sometimes he even blocks things with it." }, { id: 'orc_rider', name: 'Orc Wolf Rider', image: 'orc_rider', description: "A fast-moving cavalry unit that can quickly bypass your frontline forces. A high-priority target.\n\nWhy walk when you can ride a grumpy, oversized wolf? Faster than a rumor and smells twice as bad. The wolf is the real brains of this operation." }, { id: 'orc_archer', name: 'Orc Archer', image: 'orc_archer', description: "Ranged support that targets your army from a distance. While fragile, a group of Orc Archers can quickly decimate your units.\n\nHe discovered that hitting things from far away means less getting hit back. His technique involves closing his eyes and hoping for the best. It works more often than it should." }, { id: 'orc_chieftain', name: 'Orc Chieftain', image: 'orc_chieftain', description: "A powerful boss unit with massive health and high damage. A true test for your defenses that requires concentrated fire to be taken down.\n\nThe biggest, meanest, and loudest of them all. He got to be the boss by being exceptionally good at shouting and hitting things. Mostly shouting." }], 'Undead': [{ id: 'undead_soldier', name: 'Undead Soldier', image: 'unit_undead_soldier', description: "Basic undead infantry. Doesn't feel pain, doesn't need a salary, and has absolutely zero complaints about working overtime.\n\nHis warranty expired centuries ago, but he's still somehow holding together. Mostly." }, { id: 'undead_archer', name: 'Undead Archer', image: 'unit_undead_archer', description: "A skeletal archer with an unnervingly steady aim for someone who lacks eyeballs.\n\nHe might not have eyes, but he's had a literal eternity to practice. He's forgotten more about archery than most have ever known." }, { id: 'undead_horseman', name: 'Undead Horseman', image: 'unit_undead_horseman', description: "A relentless rider on a skeletal steed. Neither the rider nor the horse ever gets tired.\n\nThey say his horse is powered by pure spite. This duo will tirelessly run you down, mostly because they forgot how to stop." }, { id: 'undead_shielder', name: 'Undead Shielder', image: 'unit_undead_shielder', description: "An incredibly durable undead warrior, whose shield has become a part of its very being. It moves slowly but absorbs tremendous amounts of punishment.\n\nHe was told to 'hold the line' in a battle centuries ago. No one ever told him to stop." }, { id: 'death_knight', name: 'Death Knight', image: 'unit_death_knight', description: "An elite undead champion encased in dark armor. Slow, powerful, and utterly silent.\n\nA fallen hero who's had a very, very bad day. He's not much for conversation, preferring to let his giant sword do the talking." }, { id: 'lich', name: 'Lich', image: 'unit_lich', description: "A master of necromancy and the commander of the undead legion. Weak in direct combat, his true power lies in his aura, which can raise the fallen to fight once more.\n\nThe ultimate micromanager. He hates seeing his employees lie down on the job, so he makes sure their 'rest' is only temporary." }], 'Demons': [{ id: 'demon_soldier', name: 'Demon Soldier', image: 'unit_demon_soldier', description: "Basic infantry of Hell. It grows in power with each passing wave.\n\nIts only goal is to march forward. There is no plan B. It probably doesn't even know what a plan is." }, { id: 'demon_archer', name: 'Demon Archer', image: 'unit_demon_archer', description: "A marksman who fires bolts of pure hatred. Its power grows at a terrifying rate.\n\nIt never misses because its arrows seek their own targets. It's kind of cheating, but who are you going to complain to in Hell?" }, { id: 'demon_defender', name: 'Demon Defender', image: 'unit_demon_defender', description: "An armored monstrosity whose only purpose is to absorb damage.\n\nIts armor is forged from damned souls. It's so tough, it barely notices you're attacking it. Sometimes you have to remind it the fight is over." }, { id: 'demon_rider', name: 'Demon Rider', image: 'unit_demon_rider', description: "A fast flanking unit on a demonic beast.\n\nThe beast it rides is faster than rumors in the capital and far more deadly. They say it feeds only on the fear of its enemies." }, { id: 'demon_lord', name: 'Demon Lord', image: 'unit_demon_lord', description: "A powerful demon lord who appears to personally check on your progress.\n\nHis favorite pastime is watching the last sparks of hope fade. He does it over his morning coffee." }, { id: 'demon_tamer', name: 'Demon Beastmaster', image: 'unit_demon_tamer', description: "A beastmaster whose aura deals damage over time.\n\nHis pets are adorable, if you consider five-meter-tall monsters from hell adorable. At least you don't have to clean up after them." }] }; var isBuildPhase = false; var isInteractionLocked; var buildPhaseTimer = 0; var startGameButton; var timerText; var pathId; var buildingDeck; var spellOverlay; var spellEffectLayer; var buildingHand; var selectedSpawnPathId = ''; var laneSelectionArrows = []; var castleBuildings; var castleBuildingsLayer; var statsPanel; var statsDetailsContainer; var statsToggleButton; var isStatsPanelExpanded = false; var maxScore; var enemies; var towers; var bullets; var isArchmageOnField; var selectedTower; var selectedBuilding; var isDragging; var isDraggingSpell; var lastRecruitActionTick; var panelActionTaken; var guideScreen; var gold; var lives; var mana; var manaRegenTicker; var magicPanelManaText; var numberOfTaverns; var levelData; var isManaUnlocked; var enemiesKilled; var enemiesInCurrentWave; var levelData; var originalGameUpdate; var currentWave; var totalWaves; var waveTimer; var pendingWaveQueue; var waveInProgress; var weatherEffectLayer, nightOverlay; var waveSpawned; var nextWaveTime; var sourceTowers; var grid; var specialZoneIndicators; var wavesText; var towerLayer; var npcLayer; 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 currentLevelNumber; var castleContainer; var activeWeatherEvent = null; var viewToggleButton; var talentTreeScreen; var castleGrid; var castleBuildPanel; var magicButton; var rainIntervalId = null; var stormIntervalId = null; var fogIntervalId = null; var rainIntervalId = null; var fogIntervalId = null; var activeFogWisps = []; var magicPanelContainer; var armyButton; var armyPanelContainer; var mainActionPanel; var buildPage, magicPage, armyPage; var isMainPanelOpen = false; var currentOpenPage = 'build'; var isArmyUnlocked; var panelActionTaken; var spellPreview; var chosenHeroIdForGame; var spawnedHeroCount; 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() { for (var i = game.children.length - 1; i >= 0; i--) { if (game.children[i].isUpgradeBlocker) { game.children[i].destroy(); } } menu.destroy(); isHidingUpgradeMenu = false; isMenuTransitioning = false; // NOWE LINIE: Włączenie interaktywności dolnych paneli if (mainActionPanel) { mainActionPanel.interactive = true; } if (castleBuildPanel) { // Panel w widoku zamku castleBuildPanel.interactive = true; } } }); } function generateEndlessWave(waveNumber) { var budget = 100; if (waveNumber <= 20) { budget += waveNumber * 25; } else if (waveNumber <= 40) { budget += 20 * 25; budget += (waveNumber - 20) * 15; } else { budget += 20 * 25; budget += 20 * 15; budget += (waveNumber - 40) * 5; } var unitCosts = { 'demon_soldier': 5, 'demon_archer': 8, 'demon_defender': 10, 'demon_rider': 12, 'demon_lord': 75, 'demon_tamer': 100 }; var unitWeights = { 'demon_soldier': 10, 'demon_archer': 8, 'demon_defender': 5, 'demon_rider': 3, 'demon_lord': 2, 'demon_tamer': 1 }; var waveEnemies = []; var newSpawnQueue = []; var tamerSpawnChance = 0.33; var lordCount = 0; var weightedPool = []; for (var type in unitWeights) { if (type === 'demon_defender' && waveNumber < 4) { continue; } if (type === 'demon_rider' && waveNumber < 6) { continue; } if (type === 'demon_lord' && waveNumber < 3) { continue; } for (var i = 0; i < unitWeights[type]; i++) { weightedPool.push(type); } } while (budget > 5 && weightedPool.length > 0) { var randomType = weightedPool[Math.floor(Math.random() * weightedPool.length)]; if (randomType === 'demon_lord' && lordCount >= 3) { continue; } if (budget >= unitCosts[randomType]) { waveEnemies.push({ type: randomType }); budget -= unitCosts[randomType]; if (randomType === 'demon_lord') { lordCount++; } } else { var cheapestUnit = 'demon_soldier'; if (budget >= unitCosts[cheapestUnit]) { waveEnemies.push({ type: cheapestUnit }); budget -= unitCosts[cheapestUnit]; } else { break; } } } var allPaths = ['hellgate_left', 'hellgate_center', 'hellgate_right']; var activePaths = []; if (waveNumber >= 1 && waveNumber <= 4) { var randomIndex = Math.floor(Math.random() * allPaths.length); activePaths.push(allPaths[randomIndex]); } else if (waveNumber >= 5 && waveNumber <= 9) { var shuffled = allPaths.slice().sort(function () { return 0.5 - Math.random(); }); activePaths = shuffled.slice(0, 2); } else { activePaths = allPaths; } var spawnTime = 0; var baseInterval = 100; var reductionSteps = Math.floor((waveNumber - 1) / 2); var interval = Math.max(20, baseInterval - reductionSteps * 5); for (var i = 0; i < waveEnemies.length; i++) { var enemyData = waveEnemies[i]; var pathId = activePaths[i % activePaths.length]; var spawner = levelData.spawnPoints.find(function (sp) { return sp.id === pathId; }); if (spawner) { newSpawnQueue.push({ unitData: { spawner: pathId, type: enemyData.type, laneId: spawner.lanes[0].id }, spawnTime: spawnTime }); spawnTime += interval; } } return { queue: newSpawnQueue, paths: activePaths }; } function showPageInMainPanel(pageName) { if (buildPage) { buildPage.visible = pageName === 'build'; } if (magicPage) { magicPage.visible = pageName === 'magic'; // NOWY KOD: Kontrola widoczności tekstu many w panelu magii if (magicPanelManaText) { magicPanelManaText.visible = pageName === 'magic' && isManaUnlocked; } } if (armyPage) { armyPage.visible = pageName === 'army'; } } function toggleMainPanel(pageToShow) { var targetY; var panelShouldBeOpen; if (isMainPanelOpen && currentOpenPage === pageToShow) { panelShouldBeOpen = false; } else { panelShouldBeOpen = true; } if (panelShouldBeOpen) { showPageInMainPanel(pageToShow); currentOpenPage = pageToShow; targetY = 2732 - 250; isMainPanelOpen = true; } else { targetY = 2732 + 100; isMainPanelOpen = false; } if (mainActionPanel) { tween(mainActionPanel, { y: targetY }, { duration: 300, easing: tween.backOut }); } } function closeActiveUpgradeMenu() { if (isMenuTransitioning) { return; } game.children.forEach(function (child) { if (child instanceof UpgradeMenu) { hideUpgradeMenu(child); // Ta funkcja już wywołuje hideUpgradeMenu } }); for (var i = gateContainer.children.length - 1; i >= 0; i--) { if (gateContainer.children[i].isTowerRange) { gateContainer.removeChild(gateContainer.children[i]); } } selectedTower = null; // NOWE LINIE: Włączenie interaktywności dolnych paneli // Uwaga: hideUpgradeMenu już to robi. Ale na wypadek, gdyby to menu było zamykane w inny sposób. if (mainActionPanel) { mainActionPanel.interactive = true; } if (castleBuildPanel) { // Panel w widoku zamku castleBuildPanel.interactive = true; } } function updateUI() { goldText.setText('Gold: ' + gold); livesText.setText('Lives: ' + lives); enemiesText.setText('Enemies: ' + enemiesKilled + '/' + enemiesInCurrentWave); peopleText.setText('People: ' + people + '/' + maxPeople); if (currentLevelNumber == 7) { wavesText.setText('Wave: ' + currentWave); } else { wavesText.setText('Wave: ' + currentWave + '/' + totalWaves); } if (isManaUnlocked) { manaText.visible = true; manaText.setText('Mana: ' + mana); // NOWY KOD: Aktualizacja many w panelu magii if (magicPanelManaText) { magicPanelManaText.setText('Mana: ' + mana); // magicPanelManaText.visible = true; // Widoczność będzie kontrolowana przez showPageInMainPanel } } } function setGold(value) { gold = value; updateUI(); } function setMana(value) { if (isManaUnlocked) { mana = value; updateUI(); if (value > gold && progressionData.achievements) { if (!progressionData.achievements.totalGoldCollected) { progressionData.achievements.totalGoldCollected = 0; } progressionData.achievements.totalGoldCollected += value - gold; } } } function getTowerCost(towerType) { var cost = 0; if (TOWER_DATA[towerType] && TOWER_DATA[towerType][1]) { cost = TOWER_DATA[towerType][1].cost; } // NOWA LOGIKA: Zastosowanie bonusu z drzewka umiejętności if (progressionData.upgrades['towers_cost_1']) { var costReduction = 1 - progressionData.upgrades['towers_cost_1'] * 0.02; // 2% za poziom cost = Math.floor(cost * costReduction); } 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 }; if (progressionData.upgrades['building_cost_1']) { var costReduction = 1 - progressionData.upgrades['building_cost_1'] * 0.04; finalCost.gold = Math.floor(finalCost.gold * costReduction); } return finalCost; } function updateArmyPanelWithHero(chosenHeroId) { for (var i = armyPage.children.length - 1; i >= 0; i--) { var child = armyPage.children[i]; if (child instanceof SourceUnit || child instanceof SourceHero) { child.destroy(); } } var hero = new SourceHero(chosenHeroId); var units = [hero]; var unitTypes = Object.keys(UNIT_DATA); for (var i = 0; i < unitTypes.length; i++) { units.push(new SourceUnit(unitTypes[i])); } var iconSpacing = 280; var totalWidth = (units.length - 1) * iconSpacing; var startX = -totalWidth / 2; for (var i = 0; i < units.length; i++) { var unitButton = units[i]; unitButton.x = startX + i * iconSpacing; unitButton.y = 0; armyPage.addChild(unitButton); } } function getModifiedSpellData(spellId) { var originalData = SPELL_DATA[spellId]; var modifiedData = Object.assign({}, originalData); if (isArchmageOnField) { modifiedData.cost = Math.floor(modifiedData.cost * 0.70); } var sanctumCount = castleBuildings.filter(function (b) { return b.id === 'magic_sanctum'; }).length; if (sanctumCount > 0) { var spellBonus = Math.pow(1.10, sanctumCount); if (modifiedData.damage) { modifiedData.damage = Math.round(modifiedData.damage * spellBonus); } if (modifiedData.dotDamage) { modifiedData.dotDamage = Math.round(modifiedData.dotDamage * spellBonus); } if (modifiedData.stunDuration) { modifiedData.stunDuration = Math.round(modifiedData.stunDuration * spellBonus); } if (modifiedData.heal) { modifiedData.heal = Math.round(modifiedData.heal * spellBonus); } if (modifiedData.duration) { modifiedData.duration = Math.round(modifiedData.duration * spellBonus); } } if (progressionData.upgrades['magic_destruction_2'] && (spellId === 'fire_wall' || spellId === 'lightning_stun')) { var dmgBonus = 1 + progressionData.upgrades['magic_destruction_2'] * 0.04; if (modifiedData.damage) { modifiedData.damage *= dmgBonus; } if (modifiedData.dotDamage) { modifiedData.dotDamage *= dmgBonus; } } if (progressionData.upgrades['magic_manipulation_2']) { var costBonus = 1 - progressionData.upgrades['magic_manipulation_2'] * 0.03; modifiedData.cost = Math.floor(modifiedData.cost * costBonus); } if (spellId === 'fire_wall' && progressionData.upgrades['magic_firewall_duration_3']) { var durationBonus = 1 + progressionData.upgrades['magic_firewall_duration_3'] * 0.10; if (modifiedData.duration) { modifiedData.duration *= durationBonus; } } if (spellId === 'lightning_stun' && progressionData.upgrades['magic_storm_duration_3']) { var durationBonus = 1 + progressionData.upgrades['magic_storm_duration_3'] * 0.10; if (modifiedData.duration) { modifiedData.duration *= durationBonus; } } if (spellId === 'curse' && progressionData.upgrades['magic_curse_3']) { var curseBonus = 1 + progressionData.upgrades['magic_curse_3'] * 0.05; if (modifiedData.damageMultiplier) { modifiedData.damageMultiplier *= curseBonus; } if (modifiedData.slowFactor) { modifiedData.slowFactor = 1 - (1 - modifiedData.slowFactor) * curseBonus; } } if (progressionData.upgrades['magic_essence_3']) { var essenceBonus = 1 + progressionData.upgrades['magic_essence_3'] * 0.05; if (spellId === 'healing_field' && modifiedData.heal) { modifiedData.heal *= essenceBonus; } } return modifiedData; } function setupStatsPanel() { isStatsPanelExpanded = false; statsPanel = new Container(); statsPanel.x = 0; statsPanel.y = 0; LK.gui.top.addChild(statsPanel); var panelBg = statsPanel.attachAsset('panel_topbar_background', { anchorX: 0.5, anchorY: 0 }); panelBg.y = -20; statsPanel.hitAreaWidth = panelBg.width; statsPanel.hitAreaHeight = 300; // Zostawiamy dla pewności var maxBgHeight = 260; var minBgHeight = 120; panelBg.height = maxBgHeight; // Ustawiamy na stałe maksymalną wysokość var initialScaleY = minBgHeight / maxBgHeight; panelBg.scale.y = initialScaleY; // Zaczynamy ze zmniejszoną skalą 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('', { size: 45, fill: 0xFFD700, weight: 700 }); timerText.anchor.set(0.5, 0.5); timerText.x = 0; timerText.y = textY; timerText.visible = false; statsPanel.addChild(timerText); statsDetailsContainer = new Container(); statsDetailsContainer.y = 70; statsDetailsContainer.visible = false; statsPanel.addChild(statsDetailsContainer); var detailsTextOptions = { size: 45, fill: 0xFFFFFF, weight: 600, align: 'center' }; 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', Object.assign({}, detailsTextOptions, { fill: 0x00BFFF })); manaText.anchor.set(0.5, 0.5); manaText.x = -400; manaText.y = 90; manaText.visible = false; statsDetailsContainer.addChild(manaText); 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 = 0; statsToggleButton.y = 95; 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 targetScaleY = isStatsPanelExpanded ? 1.0 : initialScaleY; var targetArrowRotation = isStatsPanelExpanded ? -Math.PI / 2 : Math.PI / 2; var targetButtonY = isStatsPanelExpanded ? 225 : 35; if (isStatsPanelExpanded) { statsDetailsContainer.visible = true; } tween(panelBg, { scaleY: targetScaleY }, { duration: 250, easing: tween.easeOut, onFinish: function onFinish() { if (!isStatsPanelExpanded) { statsDetailsContainer.visible = false; } } }); tween(arrow, { rotation: targetArrowRotation }, { duration: 250, easing: tween.easeOut }); tween(statsToggleButton, { y: targetButtonY }, { duration: 250, easing: tween.easeOut }); }; } // --- FUNKCJE EKRANÓW --- function showSplashScreen() { var splashScreenContainer = new Container(); game.addChild(splashScreenContainer); currentScreenState = 'splash'; var transitionTimeoutId = null; var bg = splashScreenContainer.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000 }); var logo = splashScreenContainer.attachAsset('your_logo_graphic', { anchorX: 0.5, anchorY: 0.5 }); logo.x = 2048 / 2; logo.y = 2732 / 2; logo.alpha = 0; tween(logo, { alpha: 1 }, { duration: 1500, easing: tween.easeIn, onFinish: function onFinish() { var promptText = new Text2("Click anywhere to continue", { size: 50, fill: 0xAAAAAA, weight: 600 }); promptText.anchor.set(0.5, 0.5); promptText.x = 2048 / 2; promptText.y = 2732 - 200; promptText.alpha = 0; splashScreenContainer.addChild(promptText); pulsateEffect(promptText); LK.setTimeout(function () { if (splashScreenContainer.parent) { LK.playMusic('Lektor'); } }, 3000); var soundDuration = 18678; var transitionDelay = 3000; var performTransition = function performTransition() { if (!splashScreenContainer.parent) { return; } LK.stopMusic(); splashScreenContainer.interactive = false; tween(splashScreenContainer, { alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { splashScreenContainer.destroy(); showTitleScreen(); } }); }; transitionTimeoutId = LK.setTimeout(performTransition, transitionDelay + soundDuration); splashScreenContainer.interactive = true; splashScreenContainer.down = function () { if (transitionTimeoutId) { LK.clearTimeout(transitionTimeoutId); } performTransition(); }; } }); } function showTitleScreen() { if (titleScreenContainer) { titleScreenContainer.destroy(); } titleScreenContainer = new Container(); game.addChild(titleScreenContainer); currentScreenState = 'title'; LK.playMusic('menu'); 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; 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.down = function () { gameTitleGraphic.destroy(); startButton.destroy(); showLevelSelectScreen(); }; tween(titleScreenContainer, { alpha: 1 }, { duration: 1000, easing: tween.easeIn }); function animateBg() { if (!bg.parent) { return; } tween(bg, { x: 0 }, { duration: 9000, easing: tween.easeInOut, onFinish: function onFinish() { tween(bg, { x: -100 }, { duration: 9000, easing: tween.easeInOut, onFinish: animateBg }); } }); } animateBg(); } function pulsateEffect(target) { tween(target, { alpha: 0.6 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { tween(target, { alpha: 0 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { pulsateEffect(target); } }); } }); } function showLevelSelectScreen() { currentScreenState = 'levelSelect'; talentTreeScreen = new TalentTreeScreen(); game.addChild(talentTreeScreen); guideScreen = new GuideScreen(); game.addChild(guideScreen); var unitspediaScreen = new UnitspediaScreen(); game.addChild(unitspediaScreen); var achievementsScreen = new AchievementsScreen(); game.addChild(achievementsScreen); var parchment = titleScreenContainer.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; titleScreenContainer.addChild(selectTitle); var upgradesButton = new Container(); upgradesButton.x = 2048 / 2; upgradesButton.y = 600; titleScreenContainer.addChild(upgradesButton); var buttonBg = upgradesButton.attachAsset('upgrade_menu_button', { anchorX: 0.5, anchorY: 0.5 }); var buttonGlow = upgradesButton.attachAsset('upgrade_menu_button', { anchorX: 0.5, anchorY: 0.5, alpha: 0, blendMode: 1 }); var textGraphic = upgradesButton.attachAsset('skill_tree_text', { anchorX: 0.5, anchorY: 0.5, tint: 0x514a4a }); upgradesButton.interactive = true; upgradesButton.down = function () { titleScreenContainer.visible = false; talentTreeScreen.show(); }; pulsateEffect(buttonGlow); var guideButton = new Container(); guideButton.x = 2048 / 2 - 340; guideButton.y = 2732 - 400; titleScreenContainer.addChild(guideButton); var guideButtonBg = guideButton.attachAsset('button_guide', { anchorX: 0.5, anchorY: 0.5 }); var guideButtonGlow = guideButton.attachAsset('button_guide', { anchorX: 0.5, anchorY: 0.5, alpha: 0, blendMode: 1 }); var guideTextGraphic = guideButton.attachAsset('guide_text_graphic', { anchorX: 0.5, anchorY: 0.5, tint: 0x000000 }); guideButton.interactive = true; guideButton.down = function () { guideScreen.show(); }; pulsateEffect(guideButtonGlow); var unitspediaButton = new Container(); unitspediaButton.x = 2048 / 2 + 280; unitspediaButton.y = 2732 - 400; titleScreenContainer.addChild(unitspediaButton); var unitspediaButtonBg = unitspediaButton.attachAsset('button_unitspedia', { anchorX: 0.5, anchorY: 0.5 }); var unitspediaButtonGlow = unitspediaButton.attachAsset('button_unitspedia', { anchorX: 0.5, anchorY: 0.5, alpha: 0, blendMode: 1 }); var unitspediaTextGraphic = unitspediaButton.attachAsset('unitspedia_text_graphic', { anchorX: 0.5, anchorY: 0.5, tint: 0x000000 }); unitspediaButton.interactive = true; unitspediaButton.down = function () { titleScreenContainer.visible = false; unitspediaScreen.show(); }; pulsateEffect(unitspediaButtonGlow); var achievementsButton = new Container(); achievementsButton.x = 2048 - 200; achievementsButton.y = 200; titleScreenContainer.addChild(achievementsButton); var achievementsButtonBg = achievementsButton.attachAsset('button_achievements', { anchorX: 0.5, anchorY: 0.5 }); var achievementsButtonGlow = achievementsButton.attachAsset('button_achievements', { anchorX: 0.5, anchorY: 0.5, alpha: 0, blendMode: 1 }); achievementsButton.interactive = true; achievementsButton.down = function () { titleScreenContainer.visible = false; achievementsScreen.show(); }; pulsateEffect(achievementsButtonGlow); var totalLevelSlots = 7; var levelKeys = Object.keys(LEVEL_DATA_ALL); var startY = 850; var buttonSpacing = 190; for (var i = 0; i < totalLevelSlots; i++) { var levelKey = (i + 1).toString(); var levelButton = new Container(); levelButton.x = 2048 / 2; levelButton.y = startY + i * buttonSpacing; titleScreenContainer.addChild(levelButton); if (LEVEL_DATA_ALL[levelKey]) { var levelData = LEVEL_DATA_ALL[levelKey]; var assetName; if (levelKey === '7') { assetName = 'button_level_hellgates'; } else if (i < 3) { assetName = 'button_level_set_1'; } else { assetName = 'button_level_set_2'; } var levelButtonBg = levelButton.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5, width: 800 }); if (assetName === 'button_level_hellgates') { var hellgatesGlow = levelButton.attachAsset('button_level_hellgates', { anchorX: 0.5, anchorY: 0.5, width: 800, alpha: 0, blendMode: 1 }); pulsateEffect(hellgatesGlow); } var levelButtonText = new Text2(levelData.levelName, { size: 75, weight: 900, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 9 }); levelButtonText.anchor.set(0.5, 0.5); levelButton.addChild(levelButtonText); levelButton.interactive = true; (function (key) { levelButton.down = function () { if (key === '7') { var infoScreen = new HellgateInfoScreen(); game.addChild(infoScreen); } else { LK.stopMusic(); if (titleScreenContainer) { titleScreenContainer.destroy(); } startGame(key); } }; })(levelKey); } else if (levelKey === '7') { var hellgatesButtonBg = levelButton.attachAsset('button_level_hellgates', { anchorX: 0.5, anchorY: 0.5, width: 800 }); var hellgatesGlow = levelButton.attachAsset('button_level_hellgates', { anchorX: 0.5, anchorY: 0.5, width: 800, alpha: 0, blendMode: 1 }); pulsateEffect(hellgatesGlow); var placeholderText = new Text2("Final update coming soon", { size: 62, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 9 }); placeholderText.anchor.set(0.5, 0.5); levelButton.addChild(placeholderText); levelButton.interactive = false; } } } function getPeopleRegenTime() { var hasInfirmary = castleBuildings.some(function (b) { return b.id === 'infirmary'; }); var baseTime = hasInfirmary ? 2.5 * 60 : 4 * 60; if (progressionData.upgrades['building_infirmary_3']) { var bonusMultiplier = 1 - progressionData.upgrades['building_infirmary_3'] * 0.10; baseTime *= bonusMultiplier; } return baseTime; } 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' || buildingType === 'training_ground' || buildingType === 'magic_sanctum') { return true; } var alreadyBuilt = castleBuildings.some(function (b) { return b.id === buildingType; }); return !alreadyBuilt; }); var handToDraw = availableBuildings.slice(); for (var i = 0; i < 4; 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 = 345; 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 getRerollCost() { var rerollCost = 30; if (progressionData.upgrades['building_reroll_3']) { var costReduction = 1 - progressionData.upgrades['building_reroll_3'] * 0.25; rerollCost = Math.floor(rerollCost * costReduction); } return rerollCost; } function placeBuilding(buildingType, gridX, gridY) { var cost = getBuildingCost(buildingType); var buildingSize = BUILDING_DATA[buildingType].size; if (gold >= cost.gold) { levelProgressFlags.buildingsBuiltThisGame[buildingType] = true; var buildArea = { x: castleGrid.x + gridX * CELL_SIZE, y: castleGrid.y + gridY * CELL_SIZE, width: buildingSize.w * CELL_SIZE, height: buildingSize.h * CELL_SIZE }; for (var i = npcs.length - 1; i >= 0; i--) { var npc = npcs[i]; if (npc.x > buildArea.x && npc.x < buildArea.x + buildArea.width && npc.y > buildArea.y && npc.y < buildArea.y + buildArea.height) { var puff = new Explosion(npc.x, npc.y, 0.5); npcLayer.addChild(puff); var validCells = []; for (var ci = 0; ci < castleGrid.cells.length; ci++) { for (var cj = 0; cj < castleGrid.cells[ci].length; cj++) { if (castleGrid.cells[ci][cj].type === 1) { validCells.push(castleGrid.cells[ci][cj]); } } } if (validCells.length > 0) { var targetCell = validCells[Math.floor(Math.random() * validCells.length)]; npc.x = castleGrid.x + targetCell.x * CELL_SIZE + CELL_SIZE / 2; npc.y = castleGrid.y + targetCell.y * CELL_SIZE + CELL_SIZE / 2; npc.state = 'IDLE'; npc.stateTimer = 60; } else { npc.destroy(); } } } 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); if (buildingType === 'home' && progressionData.achievements) { if (!progressionData.achievements.homesBuilt) { progressionData.achievements.homesBuilt = 0; } progressionData.achievements.homesBuilt++; } if (buildingType === 'smithy') { towers.forEach(function (tower) { tower.recalculateStats(); }); } 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') { var populationBonus = 5; if (progressionData.upgrades['building_population_2']) { populationBonus += progressionData.upgrades['building_population_2']; } maxPeople += populationBonus; people += populationBonus; updateUI(); var npcGraphics = ['npc_man_1', 'npc_man_2', 'npc_woman_1', 'npc_woman_2']; var validSpawnCells = []; for (var ci = 0; ci < castleGrid.cells.length; ci++) { for (var cj = 0; cj < castleGrid.cells[ci].length; cj++) { if (castleGrid.cells[ci][cj].type === 1) { validSpawnCells.push(castleGrid.cells[ci][cj]); } } } for (var k = 0; k < 2; k++) { if (validSpawnCells.length > 0) { var spawnCell = validSpawnCells[Math.floor(Math.random() * validSpawnCells.length)]; var spawnX = castleGrid.x + spawnCell.x * CELL_SIZE + CELL_SIZE / 2; var spawnY = castleGrid.y + spawnCell.y * CELL_SIZE + CELL_SIZE / 2; var randomGraphic = npcGraphics[Math.floor(Math.random() * npcGraphics.length)]; var newNpc = new NPC(spawnX, spawnY, randomGraphic); npcs.push(newNpc); npcLayer.addChild(newNpc); } } } if (buildingType === 'magic_academy' && !isManaUnlocked) { isManaUnlocked = true; manaText.visible = true; magicButton.visible = true; updateUI(); } if (buildingType === 'barracks' && !isArmyUnlocked) { isArmyUnlocked = true; armyButton.visible = true; } if (buildingType === 'altar_of_champions') { isInteractionLocked = true; game.addChild(new HeroSelectionScreen(function (chosenHeroId) { updateArmyPanelWithHero(chosenHeroId); isInteractionLocked = false; })); } 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) { levelProgressFlags.towersBuilt++; var tower = new Tower(towerType || 'default'); tower.placeOnGrid(gridX, gridY); towerLayer.addChild(tower); towers.push(tower); setGold(gold - towerCost); tower.startConstructionAnimation(5, function () {}); if (activeWeatherEvent === 'FOG' && tower.towerType === 'mage') { tower.addBuff({ source: 'FOG_DEBUFF', effect: { damage: EVENT_DATA.FOG.modifier.mageTowerDebuff } }); } return true; } else { var notification = game.addChild(new Notification("Not enough gold!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return false; } } function checkAndGrantAchievements(levelNumber) { if (!progressionData.achievements) { progressionData.achievements = {}; } var achData = progressionData.achievements; // Osiągnięcie: Foundations of Defense (2 mapy, tylko wieże poz. 1) if (levelProgressFlags.maxTowerLevelUsed === 1) { if (!achData.foundationsCompletedMaps) { achData.foundationsCompletedMaps = []; } if (!achData.foundationsCompletedMaps.includes(levelNumber)) { achData.foundationsCompletedMaps.push(levelNumber); if (achData.foundationsCompletedMaps.length >= 2 && !achData.foundationsRewarded) { progressionData.points++; achData.foundationsRewarded = true; } } } // Osiągnięcie: Advanced Engineering (6 map, wieże max poz. 2) if (levelProgressFlags.maxTowerLevelUsed <= 2) { if (!achData.advancedCompletedMaps) { achData.advancedCompletedMaps = []; } if (!achData.advancedCompletedMaps.includes(levelNumber)) { achData.advancedCompletedMaps.push(levelNumber); if (achData.advancedCompletedMaps.length >= 6 && !achData.advancedRewarded) { progressionData.points++; achData.advancedRewarded = true; } } } // Osiągnięcie: Minimalist Fortress (max 10 wież na 6 mapach) if (levelProgressFlags.towersBuilt <= 10) { if (!achData.minimalistCompletedMaps) { achData.minimalistCompletedMaps = []; } if (!achData.minimalistCompletedMaps.includes(levelNumber)) { achData.minimalistCompletedMaps.push(levelNumber); if (achData.minimalistCompletedMaps.length >= 6 && !achData.minimalistRewarded) { progressionData.points++; achData.minimalistRewarded = true; } } } // Osiągnięcie: Lone Defender (bez armii na 6 mapach) if (!levelProgressFlags.wasArmyRecruited) { if (!achData.loneDefenderCompletedMaps) { achData.loneDefenderCompletedMaps = []; } if (!achData.loneDefenderCompletedMaps.includes(levelNumber)) { achData.loneDefenderCompletedMaps.push(levelNumber); if (achData.loneDefenderCompletedMaps.length >= 6 && !achData.loneDefenderRewarded) { progressionData.points++; achData.loneDefenderRewarded = true; } } } // Osiągnięcie: Arcane Abstinence (bez magii na 6 mapach) if (!levelProgressFlags.wasMagicUsed) { if (!achData.arcaneAbstinenceCompletedMaps) { achData.arcaneAbstinenceCompletedMaps = []; } if (!achData.arcaneAbstinenceCompletedMaps.includes(levelNumber)) { achData.arcaneAbstinenceCompletedMaps.push(levelNumber); if (achData.arcaneAbstinenceCompletedMaps.length >= 6 && !achData.arcaneAbstinenceRewarded) { progressionData.points++; achData.arcaneAbstinenceRewarded = true; } } } // Osiągnięcie: What Does This Do? (wszystkie czary w 1 grze) var totalSpellTypes = Object.keys(SPELL_DATA).length; if (Object.keys(levelProgressFlags.spellsUsed).length >= totalSpellTypes) { if (!achData.whatDoesThisDoRewarded) { progressionData.points++; achData.whatDoesThisDoRewarded = true; } } if (levelNumber == 6) { // Sprawdzamy tylko po ukończeniu ostatniej mapy var totalBuildingTypes = Object.keys(BUILDING_DATA).length; if (Object.keys(levelProgressFlags.buildingsBuiltThisGame).length >= totalBuildingTypes) { if (!achData.masterArchitectRewarded) { progressionData.points++; achData.masterArchitectRewarded = true; } } } saveProgression(); } function initializeGameState(levelData) { pathId = 1; maxScore = 0; enemies = []; towers = []; chosenHeroIdForGame = null; spawnedHeroCount = 0; bullets = []; friendlyUnits = []; peopleRegenQueue = []; isArchmageOnField = false; activeSpells = []; pendingWaveQueue = []; npcs = []; specialZoneIndicators = []; selectedTower = null; castleBuildings = []; var initialGold = levelData.initialGold; if (progressionData.upgrades['building_gold_4']) { initialGold += 500; } gold = initialGold; var initialLives = levelData.initialLives; if (progressionData.upgrades['building_lives_4']) { initialLives += 10; } lives = initialLives; mana = 0; manaRegenTicker = 0; isManaUnlocked = false; isArmyUnlocked = false; enemiesKilled = 0; enemiesInCurrentWave = 0; currentWave = 1; totalWaves = levelData.waves.length; waveInProgress = false; waveSpawned = false; waveTimer = 0; nextWaveTime = 1200; sourceTowers = []; people = 5; maxPeople = 5; currentActiveView = 'gate'; lastRecruitActionTick = 0; isBuildPhase = false; isInteractionLocked = true; buildPhaseTimer = 0; panelActionTaken = false; isDragging = false; isDraggingSpell = false; selectedBuilding = null; currentOpenPage = 'build'; isMainPanelOpen = false; isBuildPanelOpen = false; isMagicPanelOpen = false; isArmyPanelOpen = false; levelProgressFlags = { maxTowerLevelUsed: 1, towersBuilt: 0, wasArmyRecruited: false, spellsUsed: {}, buildingsBuiltThisGame: {}, wasMagicUsed: false }; } function setupScene(levelData) { gateContainer = new Container(); game.addChild(gateContainer); castleContainer = new Container(); npcLayer = new Container(); castleBuildingsLayer = new Container(); castleContainer.addChild(castleBuildingsLayer); castleContainer.addChild(npcLayer); game.addChild(castleContainer); castleContainer.visible = false; var castleBgAssetId; var levelNum = parseInt(currentLevelNumber); if (levelNum == 7) { castleBgAssetId = 'castle_bg_image_hell'; } else if (levelNum >= 4 && levelNum <= 6) { castleBgAssetId = 'castle_bg_image_winter'; } else { castleBgAssetId = 'castle_bg_image'; } var castleMainBg = LK.getAsset(castleBgAssetId, {}); castleContainer.addChildAt(castleMainBg, 0); castleMainBg.x = -100; castleMainBg.y = -300; gateContainer.addChild(LK.getAsset(levelData.mapAsset, { 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); weatherEffectLayer = new Container(); spellOverlay = new Container(); spellOverlay.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000 }); spellOverlay.alpha = 0; spellOverlay.visible = false; spellEffectLayer = new Container(); nightOverlay = new Container(); nightOverlay.attachAsset('cell', { width: 2048, height: 2732, tint: 0x000000 }); nightOverlay.alpha = 0; nightOverlay.visible = false; gateContainer.addChild(buildableTilesLayer); gateContainer.addChild(debugLayer); gateContainer.addChild(specialTilesLayer); gateContainer.addChild(towerLayer); gateContainer.addChild(friendlyUnitLayer); gateContainer.addChild(enemyLayer); gateContainer.addChild(weatherEffectLayer); gateContainer.addChild(spellOverlay); gateContainer.addChild(spellEffectLayer); gateContainer.addChild(nightOverlay); buildableTilesLayer.visible = false; } function setupGrid(levelData) { grid = new Grid(26, 29 + 6); grid.x = levelData.gridStartX || 140; grid.y = levelData.gridStartY || 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' || tileChar === 'C') { cell.specialEffect = tileChar === 'A' ? 'amplifier' : 'catalyst'; var isTopLeft = (x === 0 || levelData.mapLayout[y][x - 1] !== tileChar) && (y === 0 || !levelData.mapLayout[y - 1] || levelData.mapLayout[y - 1][x] !== tileChar); if (isTopLeft) { var zoneWidth = 1; var zoneHeight = 1; while (x + zoneWidth < levelData.mapLayout[y].length && levelData.mapLayout[y][x + zoneWidth] === tileChar) { zoneWidth++; } while (y + zoneHeight < levelData.mapLayout.length && levelData.mapLayout[y + zoneHeight][x] === tileChar) { zoneHeight++; } var zoneSize = Math.max(zoneWidth, zoneHeight); var assetName = tileChar === 'A' ? 'buff_amplifier_zone' : 'buff_catalyst_zone'; var zoneIndicator = new Container(); var indicatorGraphic = zoneIndicator.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5, width: CELL_SIZE * (zoneSize + 0.5), height: CELL_SIZE * (zoneSize + 0.5), alpha: 0.7 }); zoneIndicator.x = grid.x + x * CELL_SIZE + CELL_SIZE * zoneWidth / 2; zoneIndicator.y = grid.y + (y + 5) * CELL_SIZE + CELL_SIZE * zoneHeight / 2; specialTilesLayer.addChild(zoneIndicator); specialZoneIndicators.push(zoneIndicator); } } var tilePlaceholder = buildableTilesLayer.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5 }); tilePlaceholder.alpha = 0.25; if (currentLevelNumber >= 4) { tilePlaceholder.tint = 0xFFFF00; } 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; if (currentLevelNumber >= 4) { tilePlaceholder.tint = 0xFFFF00; } 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('button_start_wave', { anchorX: 0.5, anchorY: 0.5 }); startGameButton.x = 2048 / 2; startGameButton.y = 2732 / 2; gateContainer.addChild(startGameButton); startGameButton.interactive = true; startGameButton.down = function () { var gameplayMusic = ['music_gameplay_1', 'music_gameplay_2']; var chosenMusic = gameplayMusic[Math.floor(Math.random() * gameplayMusic.length)]; LK.playMusic(chosenMusic); if (currentLevelNumber == 7) { var waveData = generateEndlessWave(currentWave); pendingWaveQueue = waveData.queue; showWaveNotification(waveData.paths, currentWave); isBuildPhase = true; buildPhaseTimer = 60 * 15; timerText.visible = true; isInteractionLocked = false; } else { var weatherSelection = new WeatherSelection(currentLevelNumber, function () { isBuildPhase = true; var levelNum = parseInt(currentLevelNumber); if (levelNum === 3 || levelNum === 5) { buildPhaseTimer = 60 * 35; } else { buildPhaseTimer = 60 * 20; } timerText.visible = true; isInteractionLocked = false; }); game.addChild(weatherSelection); } startGameButton.destroy(); }; castleBuildPanel = new Container(); castleContainer.addChild(castleBuildPanel); castleBuildPanel.x = 2048 / 2; castleBuildPanel.y = 2732 + 100; var castlePanelBackground = castleBuildPanel.attachAsset('panel_popup_background', { anchorX: 0.5, anchorY: 0.5, width: 2150, height: 400 }); mainActionPanel = new Container(); gateContainer.addChild(mainActionPanel); mainActionPanel.x = 2048 / 2; mainActionPanel.y = 2732 + 100; var panelBackground = mainActionPanel.attachAsset('panel_popup_background', { anchorX: 0.5, anchorY: 0.5, width: 2150, height: 400 }); magicPanelManaText = new Text2('Mana: 0', { size: 70, fill: 0x00BFFF, weight: 800, align: 'center', stroke: 0x000000, strokeThickness: 8 }); magicPanelManaText.anchor.set(0.5, 0.5); magicPanelManaText.x = 0; magicPanelManaText.y = -panelBackground.height / 2 - 50; mainActionPanel.addChild(magicPanelManaText); magicPanelManaText.visible = false; buildPage = new Container(); mainActionPanel.addChild(buildPage); var towerTypes = ['guard', 'crossbow', 'mage', 'banner']; var towerIconSpacing = 370; 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; buildPage.addChild(tower); sourceTowers.push(tower); } magicPage = new Container(); mainActionPanel.addChild(magicPage); 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; magicPage.addChild(spell); } armyPage = new Container(); mainActionPanel.addChild(armyPage); 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; armyPage.addChild(unit); } if (levelData.spawnPoints.length > 1) { var updateArrowSelection = function updateArrowSelection() { laneSelectionArrows.forEach(function (arrow) { var isSelected = arrow.pathId === selectedSpawnPathId; tween(arrow, { scaleX: isSelected ? 3.2 : 2.7, scaleY: isSelected ? 3.2 : 2.7 }, { duration: 200, easing: tween.easeOut }); // Zmiana koloru strzałki na żółty, jeśli jest wybrana arrow.children[0].tint = isSelected ? 0xFFFF00 : [4, 5, 6, 7].includes(parseInt(currentLevelNumber)) ? 0x000000 : 0xFFFFFF; arrow.alpha = isSelected ? 1.0 : 0.8; }); }; var levelNum = parseInt(currentLevelNumber); var arrowColor = 0xFFFFFF; if ([4, 5, 6, 7].includes(levelNum)) { arrowColor = 0x000000; } if (levelData.spawnPoints.length === 3) { selectedSpawnPathId = levelData.spawnPoints[1].id; var arrowLeft = new Container(); arrowLeft.pathId = levelData.spawnPoints[0].id; arrowLeft.attachAsset('player_arrow', { anchorX: 0.5, anchorY: 0.5, rotation: Math.PI, tint: arrowColor }); arrowLeft.x = -240; arrowLeft.y = -160; armyPage.addChild(arrowLeft); var arrowCenter = new Container(); arrowCenter.pathId = levelData.spawnPoints[1].id; arrowCenter.attachAsset('player_arrow', { anchorX: 0.5, anchorY: 0.5, rotation: -Math.PI / 2, tint: arrowColor }); arrowCenter.x = 0; arrowCenter.y = -200; armyPage.addChild(arrowCenter); var arrowRight = new Container(); arrowRight.pathId = levelData.spawnPoints[2].id; arrowRight.attachAsset('player_arrow', { anchorX: 0.5, anchorY: 0.5, rotation: 0, tint: arrowColor }); arrowRight.x = 240; arrowRight.y = -160; armyPage.addChild(arrowRight); laneSelectionArrows = [arrowLeft, arrowCenter, arrowRight]; arrowLeft.down = function () { selectedSpawnPathId = arrowLeft.pathId; updateArrowSelection(); }; arrowCenter.down = function () { selectedSpawnPathId = arrowCenter.pathId; updateArrowSelection(); }; arrowRight.down = function () { selectedSpawnPathId = arrowRight.pathId; updateArrowSelection(); }; } else { selectedSpawnPathId = levelData.spawnPoints[0].id; var arrowLeft = new Container(); arrowLeft.pathId = levelData.spawnPoints[0].id; arrowLeft.attachAsset('player_arrow', { anchorX: 0.5, anchorY: 0.5, rotation: Math.PI, tint: arrowColor }); arrowLeft.x = -100; arrowLeft.y = -150; armyPage.addChild(arrowLeft); var arrowRight = new Container(); arrowRight.pathId = levelData.spawnPoints[1].id; arrowRight.attachAsset('player_arrow', { anchorX: 0.5, anchorY: 0.5, rotation: 0, tint: arrowColor }); arrowRight.x = 100; arrowRight.y = -150; armyPage.addChild(arrowRight); laneSelectionArrows = [arrowLeft, arrowRight]; arrowLeft.down = function () { selectedSpawnPathId = arrowLeft.pathId; updateArrowSelection(); }; arrowRight.down = function () { selectedSpawnPathId = arrowRight.pathId; updateArrowSelection(); }; } updateArrowSelection(); } showPageInMainPanel('build'); buildButton = new Container(); game.addChild(buildButton); buildButton.interactive = true; buildButton.x = 2048 / 2 - 400; buildButton.y = 2732 - 100; var buildButtonBg = buildButton.attachAsset('button_build', { anchorX: 0.5, anchorY: 0.5 }); buildButton.down = function () { if (currentActiveView === 'gate') { toggleMainPanel('build'); } 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('button_magic', { anchorX: 0.5, anchorY: 0.5 }); magicButton.down = function () { if (currentActiveView === 'gate') { toggleMainPanel('magic'); } }; 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('button_army', { anchorX: 0.5, anchorY: 0.5 }); armyButton.down = function () { if (currentActiveView === 'gate') { toggleMainPanel('army'); } }; viewToggleButton = new Container(); game.addChild(viewToggleButton); viewToggleButton.interactive = true; viewToggleButton.x = 250; viewToggleButton.y = 2732 - 100; var viewToggleButtonBg = viewToggleButton.attachAsset('button_castle', { anchorX: 0.5, anchorY: 0.5 }); viewToggleButton.down = function () { if (currentActiveView === 'gate') { currentActiveView = 'castle'; rerollButton.visible = true; gateContainer.visible = false; castleContainer.visible = true; } else { currentActiveView = 'gate'; rerollButton.visible = false; gateContainer.visible = true; castleContainer.visible = false; } }; rerollButton = new Container(); game.addChild(rerollButton); rerollButton.interactive = true; rerollButton.x = 2048 - 300; rerollButton.y = 2732 - 100; rerollButton.visible = false; var rerollButtonBg = rerollButton.attachAsset('button_reroll', { anchorX: 0.5, anchorY: 0.5 }); var rerollCostTextShadow = new Text2("", { size: 60, fill: 0x000000, weight: 800 }); rerollCostTextShadow.anchor.set(0, 0.5); rerollCostTextShadow.x = rerollButtonBg.x + rerollButtonBg.width / 2 + 18; rerollCostTextShadow.y = rerollButtonBg.y + 3; rerollButton.addChild(rerollCostTextShadow); var rerollCostText = new Text2("", { size: 60, fill: 0xFFD700, weight: 800 }); rerollCostText.anchor.set(0, 0.5); rerollCostText.x = rerollButtonBg.x + rerollButtonBg.width / 2 + 15; rerollCostText.y = rerollButtonBg.y; rerollButton.addChild(rerollCostText); rerollButton.update = function () { var cost = getRerollCost(); var costString = cost + 'g'; rerollCostText.setText(costString); rerollCostTextShadow.setText(costString); rerollButton.alpha = gold >= cost ? 1.0 : 0.6; }; rerollButton.down = function () { if (currentActiveView === 'castle') { var rerollCost = getRerollCost(); if (gold >= rerollCost) { setGold(gold - rerollCost); drawNewBuildingHand(); } else { var notification = game.addChild(new Notification("Not enough gold for reroll!")); notification.x = 2048 / 2; notification.y = grid.height - 50; } } }; 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', 'training_ground', 'magic_sanctum', 'altar_of_champions']; 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) { currentLevelNumber = levelNumber; game.removeChildren(); if (LK.gui.top) { LK.gui.top.removeChildren(); } 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) { return; } initializeGameState(levelData); setupScene(levelData); setupGrid(levelData); if (levelData.spawnPoints) { levelData.spawnPoints.forEach(function (spawner) { spawner.lanes.forEach(function (lane) { var pathId = spawner.id + "_" + lane.id; var goalCell = grid.getCell(spawner.goalX, spawner.goalY); if (goalCell && (goalCell.type === 0 || goalCell.type === 4)) { grid.calculatePath(pathId, goalCell); } }); if ([2, 4, 5, 6].includes(parseInt(currentLevelNumber))) { var friendlyPathId = spawner.id + "_friendly"; var friendlyGoalCell = grid.getCell(spawner.startX, spawner.startY); if (friendlyGoalCell && (friendlyGoalCell.type === 0 || friendlyGoalCell.type === 3)) { grid.calculatePath(friendlyPathId, friendlyGoalCell); } } }); } setupStatsPanel(); setupUIAndPanels(); game.update = originalGameUpdate; } function showEndScreen(isVictory, wavesSurvived) { var levelNumber = currentLevelNumber; if (isVictory && levelNumber != 7) { checkAndGrantAchievements(levelNumber); } var pointsEarnedThisRun = 0; if (wavesSurvived >= 2) { pointsEarnedThisRun += 1; } if (wavesSurvived >= 4) { pointsEarnedThisRun += 2; } if (wavesSurvived >= 6) { pointsEarnedThisRun += 3; } var pointsAlreadyEarned = progressionData.pointsPerLevel[levelNumber] || 0; var levelCap; var levelNum = parseInt(levelNumber); if (levelNum >= 4) { levelCap = 12; } else { levelCap = 15; } var pointsPossibleToEarn = levelCap - pointsAlreadyEarned; var finalPointsToAward = Math.min(pointsEarnedThisRun, pointsPossibleToEarn); if (finalPointsToAward > 0) { progressionData.points += finalPointsToAward; progressionData.pointsPerLevel[levelNumber] = (progressionData.pointsPerLevel[levelNumber] || 0) + finalPointsToAward; } var totalForMap = progressionData.pointsPerLevel[levelNumber] || 0; game.update = function () {}; var screen = new EndScreen(isVictory, finalPointsToAward, totalForMap, levelCap); game.addChild(screen); } function handlePanelClick(x, y) { var activePanel = null; if (currentActiveView === 'gate' && isMainPanelOpen) { activePanel = mainActionPanel; } else if (currentActiveView === 'castle' && isBuildPanelOpen) { activePanel = castleBuildPanel; } if (!activePanel) { return false; } var itemsContainer = null; if (activePanel === mainActionPanel) { if (buildPage.visible) { itemsContainer = buildPage; } else if (magicPage.visible) { itemsContainer = magicPage; } else if (armyPage.visible) { itemsContainer = armyPage; } } else { itemsContainer = castleBuildPanel; } if (!itemsContainer) { return false; } var localClickPos = { x: x - activePanel.x, y: y - activePanel.y }; for (var i = 0; i < itemsContainer.children.length; i++) { var item = itemsContainer.children[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) { console.log("DEBUG: Kliknięto na item w panelu!", item); 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) { if (activeWeatherEvent === 'FOG') { var notification = game.addChild(new Notification("Magic is disabled during a fog!")); notification.x = 2048 / 2; notification.y = grid.height - 50; return true; } var spellData = getModifiedSpellData(item.spellId); if (mana >= spellData.cost) { levelProgressFlags.spellsUsed[spellPreview.spellId] = true; 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) { levelProgressFlags.wasMagicUsed = true; isDraggingSpell = false; spellPreview.visible = false; spellPreview.children[0].tint = 0x00BFFF; LK.setTimeout(function () { hideSpellOverlay(); }, 1500); var spellData = getModifiedSpellData(spellPreview.spellId); if (mana >= spellData.cost) { var spellEffect; // NOWA LOGIKA: Szansa na darmowy czar var castIsFree = false; if (progressionData.upgrades['magic_freecast_4']) { if (Math.random() < 0.30) { castIsFree = true; } } if (!castIsFree) { setMana(mana - spellData.cost); } 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) { 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 { switch (spellPreview.spellId) { case 'fire_wall': spellEffect = new FireWall(x, y, spellData); var visualSize = spellData.radius * 1.5; var visualCenter = new FireWallVisual(x, y - 90, visualSize); var visualBottom = new FireWallVisual(x, y + spellData.radius * 0.8 - 90, visualSize); spellEffectLayer.addChild(visualCenter); spellEffectLayer.addChild(visualBottom); 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 && obj.parent === startGameButton) { obj.down(); return; } if (isInteractionLocked) { 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 dragOffsetX; var dragOffsetY; if (currentActiveView === 'gate' && placementPreview.itemType === 'tower') { dragOffsetX = 50; dragOffsetY = 20; } else if (currentActiveView === 'castle' && placementPreview.itemType === 'building') { // Edytuj te wartości bezpośrednio tutaj, aby dostosować przesunięcie w Castle dragOffsetX = 50; // Offset X dla budynków w Castle dragOffsetY = 100; // Offset Y dla budynków w Castle } else { dragOffsetX = 50; dragOffsetY = 100; } placementPreview.snapToGrid(x + dragOffsetX, y + dragOffsetY, 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; if (currentLevelNumber == 7) { spawnQueue = pendingWaveQueue; enemiesInCurrentWave = spawnQueue.length; } else { var waveData = levelData.waves[currentWave - 1]; if (waveData && waveData.phases) { var allEnemiesInWave = []; var currentTimeOffset = 0; waveData.phases.forEach(function (phase) { currentTimeOffset += phase.delay || 0; phase.units.forEach(function (unitGroup) { var spawner = levelData.spawnPoints.find(function (sp) { return sp.id === unitGroup.spawner; }); if (!spawner) { return; } // NOWA LOGIKA SPAWNOWANIA if (currentLevelNumber == 2 && currentWave >= 3) { // Tryb parzysty dla Mapy 2, Fali 3+ for (var i = 0; i < unitGroup.count; i += 2) { var spawnTime = currentTimeOffset + i / 2 * (phase.interval || 60); var lane1 = spawner.lanes[0]; // Lewy pas var lane2 = spawner.lanes[2]; // Prawy pas spawnQueue.push({ unitData: { spawner: unitGroup.spawner, type: unitGroup.type, laneId: lane1.id }, spawnTime: spawnTime }); allEnemiesInWave.push(unitGroup.type); if (i + 1 < unitGroup.count) { spawnQueue.push({ unitData: { spawner: unitGroup.spawner, type: unitGroup.type, laneId: lane2.id }, spawnTime: spawnTime }); allEnemiesInWave.push(unitGroup.type); } } currentTimeOffset += Math.ceil(unitGroup.count / 2) * (phase.interval || 60); } else { // Oryginalna logika dla wszystkich innych map i fal for (var i = 0; i < unitGroup.count; i++) { var spawnTime = currentTimeOffset + i * (phase.interval || 60); var lane = spawner.lanes[i % spawner.lanes.length]; spawnQueue.push({ unitData: { spawner: unitGroup.spawner, type: unitGroup.type, laneId: lane.id }, spawnTime: spawnTime }); allEnemiesInWave.push(unitGroup.type); } currentTimeOffset += unitGroup.count * (phase.interval || 60); } }); }); 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; }); var lane = spawner.lanes.find(function (l) { return l.id === spawnData.unitData.laneId; }); if (spawner && lane) { var enemy = new Enemy(spawnData.unitData.type, spawner.id, lane.id); enemy.laneOffset = lane.offset; enemy.cellX = spawner.startX; enemy.cellY = spawner.startY; enemy.x = grid.x + (enemy.cellX + enemy.laneOffset) * CELL_SIZE + CELL_SIZE / 2; enemy.y = grid.y + enemy.cellY * CELL_SIZE + CELL_SIZE / 2; enemy.waveNumber = currentWave; if (currentLevelNumber == 7) { var healthBonus = (currentWave - 1) * 19; var damageBonus = Math.floor((currentWave - 1) / 3); enemy.maxHealth += healthBonus; enemy.damage += damageBonus; } else { var healthMultiplier = Math.pow(1.10, currentWave); enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier); } if (activeWeatherEvent === 'NIGHT') { var nightMultiplier = EVENT_DATA.NIGHT.modifier.enemyStatMultiplier; enemy.maxHealth = Math.round(enemy.maxHealth * nightMultiplier); enemy.damage = Math.round(enemy.damage * nightMultiplier); } enemy.health = enemy.maxHealth; enemy.healthBar.width = enemy.health / enemy.maxHealth * 70; enemyLayerBottom.addChild(enemy); enemies.push(enemy); } } if (spawnQueue.length === 0 && enemies.length === 0) { waveInProgress = false; var tavernCount = castleBuildings.filter(function (b) { return b.id === 'tavern'; }).length; if (tavernCount > 0) { var baseIncome = 60; var incomePerPerson = 6; var totalIncome = 0; var incomeBeforeSkill = baseIncome + people * incomePerPerson; if (progressionData.upgrades['building_tavern_2']) { incomeBeforeSkill *= 1 + progressionData.upgrades['building_tavern_2'] * 0.10; } totalIncome = tavernCount * incomeBeforeSkill; setGold(gold + Math.round(totalIncome)); } clearActiveWeatherModifiers(); activeWeatherEvent = null; currentWave++; if (currentLevelNumber != 7 && currentWave > totalWaves) { showEndScreen(true, totalWaves); } else { if (currentLevelNumber == 7) { var waveData = generateEndlessWave(currentWave); pendingWaveQueue = waveData.queue; showWaveNotification(waveData.paths, currentWave); isBuildPhase = true; buildPhaseTimer = 60 * 10; timerText.visible = true; } else { var weatherSelection = new WeatherSelection(currentLevelNumber, function () { isBuildPhase = true; buildPhaseTimer = 60 * 8; timerText.visible = true; }); game.addChild(weatherSelection); LK.setTimeout(function () { if (weatherSelection && !weatherSelection.destroyed) { weatherSelection.startSelection(); } }, 2000); } } } } } 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(); } } if (currentActiveView === 'castle' && npcs) { for (var i = npcs.length - 1; i >= 0; i--) { var npc = npcs[i]; if (!npc.parent) { npcs.splice(i, 1); } else { npc.update(); } } } for (var a = enemies.length - 1; a >= 0; a--) { var enemy = enemies[a]; if (enemy.health <= 0) { var wasResurrected = false; if (!enemy.hasBeenResurrected) { var liches = enemies.filter(function (e) { return e.type === 'lich' && e.health > 0 && e !== enemy; }); for (var l = 0; l < liches.length; l++) { var lich = liches[l]; var dx = lich.x - enemy.x; var dy = lich.y - enemy.y; if (dx * dx + dy * dy < lich.auraRange * lich.auraRange) { if (Math.random() < lich.resurrectionChance) { var resurrectedEnemy = new Enemy(enemy.type, enemy.spawnerId, enemy.laneId); resurrectedEnemy.x = enemy.x; resurrectedEnemy.y = enemy.y; resurrectedEnemy.cellX = enemy.cellX; resurrectedEnemy.cellY = enemy.cellY; resurrectedEnemy.laneOffset = enemy.laneOffset; resurrectedEnemy.currentTarget = enemy.currentTarget; resurrectedEnemy.maxHealth = Math.round(resurrectedEnemy.maxHealth * lich.resurrectedStatModifier); resurrectedEnemy.health = resurrectedEnemy.maxHealth; resurrectedEnemy.damage = Math.round(resurrectedEnemy.damage * lich.resurrectedStatModifier); resurrectedEnemy.speed *= lich.resurrectedStatModifier; resurrectedEnemy.originalSpeed *= lich.resurrectedStatModifier; resurrectedEnemy.goldValue = 0; resurrectedEnemy.manaCategory = 'weak'; resurrectedEnemy.hasBeenResurrected = true; resurrectedEnemy.children[0].tint = 0x90EE90; resurrectedEnemy.children[0].alpha = 0.8; enemies.push(resurrectedEnemy); enemyLayerBottom.addChild(resurrectedEnemy); wasResurrected = true; break; } } } } enemiesKilled++; updateUI(); if (!wasResurrected) { var goldEarned = enemy.goldValue || 0; if (goldEarned > 0) { var goldMultiplier = activeWeatherEvent === 'RAIN' ? 0.8 : 1; goldEarned = Math.floor(goldEarned * goldMultiplier); setGold(gold + goldEarned); gateContainer.addChild(new GoldIndicator(goldEarned, enemy.x, enemy.y)); } if (isManaUnlocked) { var manaEarned = 0; switch (enemy.manaCategory) { case 'weak': manaEarned = 2; break; case 'strong': manaEarned = 4; break; case 'special': manaEarned = 8; break; case 'boss': manaEarned = 25; break; } if (progressionData.upgrades['magic_mana_kill_1']) { var manaBonus = 1 + progressionData.upgrades['magic_mana_kill_1'] * 0.10; manaEarned = Math.round(manaEarned * manaBonus); } setMana(mana + manaEarned); } } if (enemy.parent) { enemy.parent.removeChild(enemy); } enemies.splice(a, 1); continue; } if (enemy.update()) { if (enemy.parent) { enemy.parent.removeChild(enemy); } enemies.splice(a, 1); lives = Math.max(0, lives - 1); updateUI(); if (lives <= 0) { showEndScreen(false, currentWave - 1); } } } 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; } // NOWA LOGIKA: Pasywna regeneracja many if (isManaUnlocked && progressionData.upgrades['magic_regen_4']) { manaRegenTicker++; if (manaRegenTicker >= 60) { // 60 klatek = 1 sekunda manaRegenTicker = 0; setMana(mana + 1); } } updateGameLogic(); updateGameObjects(); updatePreviews(); if (rerollButton.visible) { rerollButton.update(); } if (specialZoneIndicators) { for (var i = 0; i < specialZoneIndicators.length; i++) { specialZoneIndicators[i].rotation += 0.001; } } }; originalGameUpdate = game.update; loadProgression(); showSplashScreen();
===================================================================
--- original.js
+++ change.js
@@ -22,14 +22,14 @@
total: 6
},
'minimalist_fortress': {
name: "Minimalist Fortress",
- description: "Complete all 6 maps (1-6) building a maximum of 10 towers on each.",
+ description: "Complete 6 campaign maps building a maximum of 10 towers on each.",
total: 6
},
'lone_defender': {
name: "Lone Defender",
- description: "Complete all 6 maps (1-6) without recruiting any army units.",
+ description: "Complete 6 campaign maps without recruiting any army units.",
total: 6
},
'arcane_abstinence': {
name: "Arcane Abstinence",
@@ -43,11 +43,11 @@
isSingle: true
},
'master_architect': {
name: "Master Architect",
- description: "Build every type of building in your castle (cumulative).",
- total: Object.keys(BUILDING_DATA).length,
- isGlobal: true
+ description: "Build all 10 unique building types in your castle in a single game.",
+ total: 1,
+ isSingle: true
},
'grand_builder': {
name: "Grand Builder",
description: "Build a total of 15 Home buildings.",
@@ -72,8 +72,15 @@
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
+ // DODANA GRAFIKA Z TYTUŁEM
+ self.attachAsset('achievements_title_graphic', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ x: 2048 / 2,
+ y: 250
+ });
var backButton = new Container();
backButton.x = 2048 - 250;
backButton.y = 150;
var backButtonBg = backButton.attachAsset('button_back_encyclopedia', {
@@ -85,9 +92,9 @@
self.hide();
};
var listContainer = new Container();
listContainer.x = 2048 / 2;
- listContainer.y = 550;
+ listContainer.y = 550; // Przesunięcie listy w dół
self.addChild(listContainer);
function renderAchievements() {
listContainer.removeChildren();
var yPos = 0;
@@ -109,8 +116,11 @@
}
if (id === 'lone_defender' && achData.loneDefenderCompletedMaps) {
progress = achData.loneDefenderCompletedMaps.length;
}
+ if (id === 'arcane_abstinence' && achData.arcaneAbstinenceCompletedMaps) {
+ progress = achData.arcaneAbstinenceCompletedMaps.length;
+ }
if (id === 'master_architect' && achData.buildingsConstructed) {
progress = Object.keys(achData.buildingsConstructed).length;
}
if (id === 'grand_builder' && achData.homesBuilt) {
@@ -127,29 +137,29 @@
}
}
progress = Math.min(progress, config.total);
var isCompleted = progress >= config.total;
- var textColor = isCompleted ? 0x00FF00 : 0x4a3c30;
+ var textColor = isCompleted ? 0x00FF00 : 0x000000;
var achContainer = new Container();
achContainer.y = yPos;
listContainer.addChild(achContainer);
var nameText = new Text2(config.name + ": " + config.description, {
- size: 54,
+ size: 56,
fill: textColor,
weight: 'normal',
wordWrap: true,
- wordWrapWidth: 1200
+ wordWrapWidth: 1000
});
nameText.anchor.set(0, 0.5);
- nameText.x = -730;
+ nameText.x = -800;
achContainer.addChild(nameText);
var progressText = new Text2(progress + " / " + config.total, {
- size: 58,
+ size: 60,
fill: textColor,
- weight: 800
+ weight: 'bold'
});
progressText.anchor.set(1, 0.5);
- progressText.x = 720;
+ progressText.x = 800;
achContainer.addChild(progressText);
yPos += 180;
}
}
@@ -935,9 +945,9 @@
anchorX: 0.5,
anchorY: 0.5,
width: self.auraRange * 2,
height: self.auraRange * 2,
- alpha: 0.4
+ alpha: 0.6
});
self.auraGraphics.blendMode = 1;
self.addChildAt(self.auraGraphics, 0);
break;
@@ -2443,21 +2453,33 @@
anchorX: 0.5,
anchorY: 0.5
});
var fullDescription = "You do not earn Skill Points in this mode.\n" + "Here, you test your build against endless waves of enemies.\n" + "With each wave, enemies become stronger\n" + "Your high score will be saved!";
- var descriptionText = new Text2(fullDescription, {
+ // Cień tekstu
+ var descriptionText_shadow = new Text2(fullDescription, {
size: 66,
- fill: 0xFFFFFF,
+ fill: 0xd9d9d9,
weight: 600,
align: 'center',
wordWrap: true,
- wordWrapWidth: 1150,
- stroke: 0x000000,
- strokeThickness: 8
+ wordWrapWidth: 1150
});
- descriptionText.anchor.set(0.5, 0.5);
- descriptionText.y = 250;
- panel.addChild(descriptionText);
+ descriptionText_shadow.anchor.set(0.5, 0.5);
+ descriptionText_shadow.x = 5; // Przesunięcie cienia w prawo
+ descriptionText_shadow.y = 250 + 5; // Przesunięcie cienia w dół
+ panel.addChild(descriptionText_shadow);
+ // Główny tekst
+ var descriptionText_main = new Text2(fullDescription, {
+ size: 66,
+ fill: 0xd90f0f,
+ weight: 600,
+ align: 'center',
+ wordWrap: true,
+ wordWrapWidth: 1150
+ });
+ descriptionText_main.anchor.set(0.5, 0.5);
+ descriptionText_main.y = 250;
+ panel.addChild(descriptionText_main);
var startButton = new Container();
startButton.y = 750;
startButton.x = -300;
panel.addChild(startButton);
@@ -5395,22 +5417,25 @@
storage.progression_points = progressionData.points;
storage.progression_upgrades = progressionData.upgrades;
storage.progression_pointsPerLevel = progressionData.pointsPerLevel;
storage.hellgates_high_score = progressionData.hellgatesHighScore;
+ storage.progression_achievements = progressionData.achievements;
}
function loadProgression() {
var loadedPoints = storage.progression_points;
var loadedUpgrades = storage.progression_upgrades;
var loadedPointsPerLevel = storage.progression_pointsPerLevel;
var loadedHighScore = storage.hellgates_high_score;
+ var loadedAchievements = storage.progression_achievements;
progressionData.points = typeof loadedPoints === 'number' ? loadedPoints : 0;
progressionData.upgrades = loadedUpgrades && _typeof(loadedUpgrades) === 'object' ? loadedUpgrades : {};
progressionData.pointsPerLevel = loadedPointsPerLevel && _typeof(loadedPointsPerLevel) === 'object' ? loadedPointsPerLevel : {
'1': 0,
'2': 0,
'3': 0
};
progressionData.hellgatesHighScore = typeof loadedHighScore === 'number' ? loadedHighScore : 0;
+ progressionData.achievements = loadedAchievements && _typeof(loadedAchievements) === 'object' ? loadedAchievements : {};
}
var talentTreeConfig = {
'Towers': [{
id: 'towers_cost_1',
@@ -6573,9 +6598,9 @@
'fire_wall': {
id: 'fire_wall',
name: 'Fire Wall',
description: 'Damages enemies in an area.',
- cost: 25,
+ cost: 35,
radius: 2.5 * CELL_SIZE,
duration: 460,
dotDamage: 9,
dotDuration: 230
@@ -6583,17 +6608,17 @@
'healing_field': {
id: 'healing_field',
name: 'Healing Field',
description: 'Heals allied targets in an area.',
- cost: 20,
+ cost: 30,
heal: 25,
radius: 3 * CELL_SIZE
},
'lightning_stun': {
id: 'lightning_stun',
name: 'Thunderstorm',
description: 'Stuns and damages enemies in an area.',
- cost: 25,
+ cost: 35,
stunDuration: 2 * 60,
radius: 3 * CELL_SIZE,
damage: 19,
duration: 110
@@ -6601,16 +6626,16 @@
'summon_golem': {
id: 'summon_golem',
name: 'Summon Golem',
description: 'Summons a temporary blocker unit.',
- cost: 30,
+ cost: 40,
radius: 0
},
'curse': {
id: 'curse',
name: 'Curse',
description: 'Cursed enemies move slower and take more damage.',
- cost: 25,
+ cost: 35,
damageMultiplier: 1.10,
slowFactor: 0.85,
duration: 480,
radius: 3.5 * CELL_SIZE
@@ -8193,21 +8218,14 @@
'demon_soldier': 10,
'demon_archer': 8,
'demon_defender': 5,
'demon_rider': 3,
- 'demon_lord': 2
+ 'demon_lord': 2,
+ 'demon_tamer': 1
};
var waveEnemies = [];
var newSpawnQueue = [];
var tamerSpawnChance = 0.33;
- if (waveNumber >= 5 && Math.random() < tamerSpawnChance) {
- if (budget >= unitCosts['demon_tamer']) {
- waveEnemies.push({
- type: 'demon_tamer'
- });
- budget -= unitCosts['demon_tamer'];
- }
- }
var lordCount = 0;
var weightedPool = [];
for (var type in unitWeights) {
if (type === 'demon_defender' && waveNumber < 4) {
@@ -9147,12 +9165,9 @@
function placeBuilding(buildingType, gridX, gridY) {
var cost = getBuildingCost(buildingType);
var buildingSize = BUILDING_DATA[buildingType].size;
if (gold >= cost.gold) {
- if (!progressionData.achievements.buildingsConstructed) {
- progressionData.achievements.buildingsConstructed = {};
- }
- progressionData.achievements.buildingsConstructed[buildingType] = true;
+ levelProgressFlags.buildingsBuiltThisGame[buildingType] = true;
var buildArea = {
x: castleGrid.x + gridX * CELL_SIZE,
y: castleGrid.y + gridY * CELL_SIZE,
width: buildingSize.w * CELL_SIZE,
@@ -9378,8 +9393,18 @@
progressionData.points++;
achData.whatDoesThisDoRewarded = true;
}
}
+ if (levelNumber == 6) {
+ // Sprawdzamy tylko po ukończeniu ostatniej mapy
+ var totalBuildingTypes = Object.keys(BUILDING_DATA).length;
+ if (Object.keys(levelProgressFlags.buildingsBuiltThisGame).length >= totalBuildingTypes) {
+ if (!achData.masterArchitectRewarded) {
+ progressionData.points++;
+ achData.masterArchitectRewarded = true;
+ }
+ }
+ }
saveProgression();
}
function initializeGameState(levelData) {
pathId = 1;