/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var AreaDamage = Container.expand(function (centerX, centerY, radius, damage, excludeEnemy) { var self = Container.call(this); self.x = centerX; self.y = centerY; self.radius = radius; self.damage = damage; self.excludeEnemy = excludeEnemy; self.lifetime = 5; // Frames to exist // Create visual effect var assetName = radius > 100 ? 'largeAreaDamage' : 'smallAreaDamage'; var visualEffect = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); visualEffect.alpha = 0.6; // Animate the visual effect tween(visualEffect, { alpha: 0, scaleX: 1.2, scaleY: 1.2 }, { duration: 2000, easing: tween.easeOut }); self.update = function () { // Deal damage to all enemies in radius for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (enemy === self.excludeEnemy) continue; // Skip the original target var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.radius) { enemy.takeDamage(self.damage); // If enemy health becomes negative, destroy immediately if (enemy.health <= 0) { enemy.die(); enemy.destroy(); for (var j = enemies.length - 1; j >= 0; j--) { if (enemies[j] === enemy) { enemies.splice(j, 1); break; } } } } } self.lifetime--; if (self.lifetime <= 0) { self.shouldDestroy = true; } }; return self; }); var Bullet = Container.expand(function (startX, startY, damage, target, bulletType) { var self = Container.call(this); self.x = startX; self.y = startY; self.speed = 8; self.damage = damage || 1; self.target = target; self.bulletType = bulletType || 'bullet'; var bulletGraphics = self.attachAsset(self.bulletType, { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (self.target && self.target.parent) { // Calculate direction to target var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 5) { // Move towards target var directionX = dx / distance; var directionY = dy / distance; self.x += directionX * self.speed; self.y += directionY * self.speed; // Store direction for straight line movement self.directionX = directionX; self.directionY = directionY; } else { // Close enough to target if (self.target.takeDamage) { self.target.takeDamage(self.damage); } self.shouldDestroy = true; } } else { // Target is gone, continue in straight line if (self.directionX !== undefined && self.directionY !== undefined) { self.x += self.directionX * self.speed; self.y += self.directionY * self.speed; } else { // No direction set, destroy bullet self.shouldDestroy = true; } } }; return self; }); var Defender = Container.expand(function (type, level) { var self = Container.call(this); self.type = type || 'green'; self.level = level || 1; self.shootTimer = 0; self.shootInterval = 60; // 1 second at 60fps self.damage = 1; var assetNames = { 'green': 'archer', 'yellow': 'spearFighter', 'red': 'musketeer', 'orange': 'cannon', 'blue': 'skyMage', 'black': 'darkMage' }; var defenderGraphics = self.attachAsset(assetNames[self.type], { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { // Check if linked die just finished rolling if (self.linkedDie && self.linkedDie.justFinishedRolling) { self.shoot(); } }; self.shoot = function () { var die = self.linkedDie; if (die && enemies.length > 0 && die.justFinishedRolling) { // Find nearest enemy var nearestEnemy = null; var shortestDistance = Infinity; for (var e = 0; e < enemies.length; e++) { var enemy = enemies[e]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < shortestDistance) { shortestDistance = distance; nearestEnemy = enemy; } } if (nearestEnemy) { var shots = die.getRollValue(); var bulletTypes = { 'green': 'arrow', 'yellow': 'spear', 'red': 'musketBall', 'orange': 'cannonBall', 'blue': 'blueMagic', 'black': 'darkMagic' }; var bulletType = bulletTypes[self.type] || 'bullet'; // Set damage based on defender type var bulletDamage = self.damage; if (self.type === 'red') { bulletDamage = 2; // Musketeer deals 2 damage } else if (self.type === 'blue') { bulletDamage = 2; // Sky mage deals 2 damage } // Fire bullets in sequence with 0.3 second intervals for (var i = 0; i < shots; i++) { LK.setTimeout(function (shotIndex) { return function () { // Find target again (in case original target was destroyed) var currentTarget = nearestEnemy; if (!currentTarget || !currentTarget.parent) { // Find new nearest enemy for (var e = 0; e < enemies.length; e++) { var enemy = enemies[e]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (!currentTarget || distance < Math.sqrt((currentTarget.x - self.x) * (currentTarget.x - self.x) + (currentTarget.y - self.y) * (currentTarget.y - self.y))) { currentTarget = enemy; } } } if (currentTarget && currentTarget.parent) { var bullet = new Bullet(self.x, self.y - 40, bulletDamage, currentTarget, bulletType); bullets.push(bullet); game.addChild(bullet); LK.getSound('shoot').play(); } }; }(i), i * 300); // 300ms = 0.3 seconds } die.justFinishedRolling = false; } } }; return self; }); var Die = Container.expand(function (type, level) { var self = Container.call(this); self.type = type || 'green'; self.level = level || 1; self.rollTimer = 0; self.rollInterval = 180; // 3 seconds at 60fps self.lastRoll = 1; self.isDragging = false; self.justFinishedRolling = false; self.originalPosition = { x: 0, y: 0 }; var assetNames = { 'green': 'greenDie', 'yellow': 'yellowDie', 'red': 'redDie', 'orange': 'orangeDie', 'blue': 'blueDie', 'black': 'blackDie' }; var dieGraphics = self.attachAsset(assetNames[self.type], { anchorX: 0.5, anchorY: 0.5 }); var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF; var levelText = new Text2(self.level.toString(), { size: 40, fill: textColor }); levelText.anchor.set(0.5, 0.5); levelText.x = 0; levelText.y = -20; self.addChild(levelText); var rollText = new Text2(self.lastRoll.toString(), { size: 60, fill: textColor }); rollText.anchor.set(0.5, 0.5); rollText.x = 0; rollText.y = 20; self.addChild(rollText); self.down = function (x, y, obj) { self.isDragging = true; self.originalPosition.x = self.x; self.originalPosition.y = self.y; draggedDie = self; }; self.update = function () { self.rollTimer++; // Blue dice rotate every 2 seconds (120 ticks), others every 3 seconds (180 ticks) var currentInterval = self.rollInterval; if (self.type === 'blue') { currentInterval = 120; // 2 seconds for blue dice } if (self.rollTimer >= currentInterval) { self.rollTimer = 0; // Unfair dice roll - higher probability for lower values var rollWeights = [35, 25, 20, 12, 6, 2]; // Weights for 1,2,3,4,5,6 var totalWeight = 0; for (var w = 0; w < rollWeights.length; w++) { totalWeight += rollWeights[w]; } var random = Math.random() * totalWeight; var currentWeight = 0; self.lastRoll = 1; // Default to 1 for (var w = 0; w < rollWeights.length; w++) { currentWeight += rollWeights[w]; if (random <= currentWeight) { self.lastRoll = w + 1; break; } } var displayValue = self.lastRoll + (self.level - 1) * 2; rollText.setText(displayValue.toString()); var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF; rollText.fill = textColor; self.justFinishedRolling = true; // Add rolling animation tween(dieGraphics, { rotation: Math.PI * 2 }, { duration: 500, easing: tween.easeOut }); } }; self.getRollValue = function () { return self.lastRoll + (self.level - 1) * 2; }; self.updateLevel = function (newLevel) { self.level = newLevel; levelText.setText(self.level.toString()); var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF; levelText.fill = textColor; rollText.fill = textColor; }; return self; }); var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'troll'; // Set stats based on enemy type var enemyStats = { 'troll': { health: 5, speed: 1, goldValue: Math.floor(Math.random() * 8) + 5, asset: 'troll', behavior: 'melee' }, 'skeleton': { health: 10, speed: 1.5, goldValue: Math.floor(Math.random() * 11) + 8, asset: 'skeleton', behavior: 'melee' }, 'knight': { health: 1, speed: 3, goldValue: Math.floor(Math.random() * 5) + 3, asset: 'knight', behavior: 'melee' }, 'witch': { health: 30, speed: 0.5, goldValue: Math.floor(Math.random() * 15) + 12, asset: 'witch', behavior: 'summoner' }, 'golem': { health: 100, speed: 0.8, goldValue: Math.floor(Math.random() * 23) + 23, asset: 'golem', behavior: 'melee' }, 'demon': { health: 75, speed: 1.2, goldValue: Math.floor(Math.random() * 18) + 15, asset: 'demon', behavior: 'ranged' } }; var stats = enemyStats[self.type] || enemyStats['troll']; self.health = stats.health; self.maxHealth = stats.health; self.speed = stats.speed; self.goldValue = stats.goldValue; self.behavior = stats.behavior; self.summonTimer = 0; self.attackTimer = 0; self.hasAttackedWall = false; self.skeletonCount = 0; // Track how many skeletons this witch has created self.spawnedSkeletons = []; // Track spawned skeletons for cleanup var enemyGraphics = self.attachAsset(stats.asset, { anchorX: 0.5, anchorY: 0.5 }); var healthBar = new Text2(self.health.toString(), { size: 30, fill: 0xFF0000 }); healthBar.anchor.set(0.5, 0.5); healthBar.x = 0; healthBar.y = -80; self.addChild(healthBar); self.update = function () { if (self.behavior === 'ranged' && self.y >= 1500) { // Demon ranged attack behavior - stop and shoot self.attackTimer++; if (self.attackTimer >= 120) { // Every 2 seconds self.attackTimer = 0; self.rangedAttack(); } } else if (self.behavior === 'summoner') { // Witch behavior - move slowly (no longer summons skeletons) if (self.y < 1770) { self.y += self.speed; } } else if (self.y >= 1770 && !self.hasAttackedWall) { // Reached wall - deal damage self.hasAttackedWall = true; self.attackWall(); } else if (self.y < 1770) { // Normal movement self.y += self.speed; } }; self.rangedAttack = function () { if (wallHealth > 0) { var projectile = new RangedProjectile(self.x, self.y, 1770); rangedProjectiles.push(projectile); game.addChild(projectile); } }; self.summonSkeleton = function () { // Check if we already have 3 skeletons if (self.skeletonCount >= 3) { return; // Don't spawn more skeletons } var skeleton = new Enemy('skeleton'); skeleton.x = self.x + (Math.random() - 0.5) * 100; skeleton.y = self.y + 50; skeleton.createdBy = self; // Link skeleton to witch enemies.push(skeleton); game.addChild(skeleton); self.skeletonCount++; self.spawnedSkeletons.push(skeleton); totalEnemyCount++; }; self.attackWall = function () { wallHealth -= 1; wallHealthText.setText('Wall HP: ' + wallHealth); LK.getSound('wallHit').play(); LK.effects.flashObject(wall, 0xFF0000, 300); if (wallHealth <= 0) { // Game over LK.effects.flashScreen(0xFF0000, 1000); LK.setTimeout(function () { LK.showGameOver(); }, 1000); } }; self.takeDamage = function (damage) { self.health -= damage; healthBar.setText(self.health.toString()); if (self.health <= 0) { self.die(); } }; self.die = function () { gold += self.goldValue; goldText.setText('Gold: ' + gold); LK.getSound('enemyDie').play(); LK.effects.flashObject(self, 0xFF0000, 500); // If this is a witch, clean up references to spawned skeletons and decrement witch count if (self.type === 'witch') { witchCount--; for (var i = 0; i < self.spawnedSkeletons.length; i++) { var skeleton = self.spawnedSkeletons[i]; if (skeleton && skeleton.createdBy === self) { skeleton.createdBy = null; } } } // Decrement golem count if this is a golem if (self.type === 'golem') { golemCount--; } // Decrement total enemy count totalEnemyCount--; }; return self; }); var RangedProjectile = Container.expand(function (startX, startY, targetY) { var self = Container.call(this); self.x = startX; self.y = startY; self.targetY = targetY; self.speed = 4; var projectileGraphics = self.attachAsset('rangedAttack', { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { self.y += self.speed; if (self.y >= self.targetY) { // Hit the wall wallHealth -= 1; // Each enemy deals 1 damage to wall wallHealthText.setText('Wall HP: ' + wallHealth); LK.getSound('wallHit').play(); LK.effects.flashObject(wall, 0xFF0000, 300); self.shouldDestroy = true; if (wallHealth <= 0) { // Game over LK.effects.flashScreen(0xFF0000, 1000); LK.setTimeout(function () { LK.showGameOver(); }, 1000); } } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2F5233 }); /**** * Game Code ****/ // Add fantasy world background image var fantasyBackground = game.addChild(LK.getAsset('fantasyWorldBackground', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 1366 })); // Game variables var gold = 10; var enemies = []; var bullets = []; var dice = []; var defenders = []; var wallSlots = []; var diceSlots = []; var draggedDie = null; var trashArea = null; var enemySpawnTimer = 0; var enemySpawnInterval = 300; // 5 seconds initially var gameTimer = 0; // Track game time in ticks var dicesPurchased = 0; // Track number of dice purchased for pricing var wallHealth = 10; var rangedProjectiles = []; var witchCount = 0; // Track number of witches currently on field var golemCount = 0; // Track number of golems currently on field var totalEnemyCount = 0; // Track total number of enemies on field // UI elements var goldText = new Text2('Gold: 10', { size: 60, fill: 0xFFFF00 }); goldText.anchor.set(0, 0); goldText.x = 150; goldText.y = 50; LK.gui.topLeft.addChild(goldText); // Wall health display var wallHealthText = new Text2('Wall HP: 10', { size: 50, fill: 0xFF0000 }); wallHealthText.anchor.set(0, 0); wallHealthText.x = 150; wallHealthText.y = 120; LK.gui.topLeft.addChild(wallHealthText); // Stage indicator display var stageText = new Text2('Stage: 1', { size: 50, fill: 0x00FF00 }); stageText.anchor.set(1, 0); stageText.x = -50; stageText.y = 50; LK.gui.topRight.addChild(stageText); // Create wall var wall = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 1, x: 1024, y: 1900 })); // Create wall slots (9 defensive positions) for (var i = 0; i < 9; i++) { var slot = game.addChild(LK.getAsset('wallSlot', { anchorX: 0.5, anchorY: 0.5, x: 200 + i * 200, y: 1900 - 75 })); wallSlots.push(slot); } // Create dice grid (3x3) var diceGridStartX = 824; // Center on screen (1024 - 200) var diceGridStartY = 2100; for (var row = 0; row < 3; row++) { for (var col = 0; col < 3; col++) { var slot = game.addChild(LK.getAsset('diceSlot', { anchorX: 0.5, anchorY: 0.5, x: diceGridStartX + col * 200, y: diceGridStartY + row * 200 })); slot.isEmpty = true; slot.gridIndex = row * 3 + col; diceSlots.push(slot); } } // Create trash area to the left of dice grid trashArea = game.addChild(LK.getAsset('trashArea', { anchorX: 0.5, anchorY: 0.5, x: 524, // Left of dice grid y: 2200 })); // Buy button var buyButton = game.addChild(LK.getAsset('buyButton', { anchorX: 0.5, anchorY: 0.5, x: 1600, y: 2200 })); // Price display text var priceText = new Text2('Price: 10', { size: 40, fill: 0xFFFFFF }); priceText.anchor.set(0.5, 0); priceText.x = 1600; priceText.y = 2400; game.addChild(priceText); buyButton.down = function (x, y, obj) { var currentPrice = 10 + dicesPurchased * 5; if (gold >= currentPrice) { buyDie(); } }; function buyDie() { var emptySlot = null; for (var i = 0; i < diceSlots.length; i++) { if (diceSlots[i].isEmpty) { emptySlot = diceSlots[i]; break; } } if (emptySlot) { var currentPrice = 10 + dicesPurchased * 5; gold -= currentPrice; dicesPurchased++; goldText.setText('Gold: ' + gold); // Update price display for next purchase var nextPrice = 10 + dicesPurchased * 5; priceText.setText('Price: ' + nextPrice); var dieTypes = ['green', 'yellow', 'red', 'orange', 'blue', 'black']; var weights = [30, 30, 20, 20, 10, 10]; // Common, Common, Rare, Rare, Legendary, Legendary var randomType = getWeightedRandom(dieTypes, weights); var newDie = new Die(randomType, 1); newDie.x = emptySlot.x; newDie.y = emptySlot.y; newDie.slotIndex = emptySlot.gridIndex; dice.push(newDie); game.addChild(newDie); emptySlot.isEmpty = false; emptySlot.occupiedBy = newDie; // Create corresponding defender createDefender(newDie); } } function getWeightedRandom(items, weights) { var totalWeight = 0; for (var i = 0; i < weights.length; i++) { totalWeight += weights[i]; } var random = Math.random() * totalWeight; var currentWeight = 0; for (var i = 0; i < items.length; i++) { currentWeight += weights[i]; if (random <= currentWeight) { return items[i]; } } return items[0]; } function createDefender(die) { var defender = new Defender(die.type, die.level); var wallSlot = wallSlots[die.slotIndex % 9]; defender.x = wallSlot.x; defender.y = wallSlot.y; defender.linkedDie = die; defenders.push(defender); game.addChild(defender); } function spawnEnemy() { // Check total enemy limit if (totalEnemyCount >= 20) { return; // Don't spawn if at maximum enemy count } var enemyType = 'troll'; var enemiesToSpawn = 1; var spawnPattern = 'single'; // Stage 1: Only trolls (0-60 seconds) - slow spawn rate if (gameTimer <= 3600) { enemyType = 'troll'; spawnPattern = 'single'; } // Stage 2: Trolls + skeletons (60-120 seconds) - faster spawn rate else if (gameTimer <= 7200) { enemyType = Math.random() < 0.6 ? 'troll' : 'skeleton'; spawnPattern = 'single'; } // Stage 3: Trolls + knights (120-165 seconds) - trolls get more health else if (gameTimer <= 9900) { enemyType = Math.random() < 0.7 ? 'troll' : 'knight'; spawnPattern = 'single'; } // Stage 4: Knights + witches + trolls (165-210 seconds) else if (gameTimer <= 12600) { var rand = Math.random(); if (rand < 0.4) enemyType = 'knight';else if (rand < 0.7 && witchCount < 4) enemyType = 'witch';else enemyType = 'troll'; spawnPattern = 'single'; } // Stage 5: Golems in front, then knights behind, then witches (210+ seconds) else { var rand = Math.random(); if (rand < 0.3 && golemCount < 3) { enemyType = 'golem'; spawnPattern = 'front'; } else if (rand < 0.7) { enemyType = 'knight'; spawnPattern = 'behind'; } else if (witchCount < 4) { enemyType = 'witch'; spawnPattern = 'back'; } else { enemyType = 'knight'; spawnPattern = 'behind'; } } // Spawn enemies based on pattern if (spawnPattern === 'single') { var enemy = new Enemy(enemyType); // Stage 3+: Give trolls and skeletons more health if (gameTimer > 7200) { if (enemyType === 'troll') { enemy.health = 10; enemy.maxHealth = 10; enemy.children[1].setText(enemy.health.toString()); // Update health display } else if (enemyType === 'skeleton') { enemy.health = 15; enemy.maxHealth = 15; enemy.children[1].setText(enemy.health.toString()); // Update health display } } enemy.x = Math.random() * 1688 + 180; // Random X position (avoiding 80px margins) enemy.y = -100; // Start above screen if (enemyType === 'witch') { witchCount++; // Spawn 2 skeletons with 25 health each in front of witch for (var s = 0; s < 2; s++) { var skeleton = new Enemy('skeleton'); skeleton.health = 25; skeleton.maxHealth = 25; skeleton.children[1].setText(skeleton.health.toString()); // Update health display skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch skeleton.y = enemy.y + 100; // Position skeletons in front of witch skeleton.createdBy = enemy; // Link skeleton to witch enemies.push(skeleton); game.addChild(skeleton); enemy.skeletonCount++; enemy.spawnedSkeletons.push(skeleton); totalEnemyCount++; } } if (enemyType === 'golem') { golemCount++; } totalEnemyCount++; enemies.push(enemy); game.addChild(enemy); } else if (spawnPattern === 'front') { // Spawn golem at front var enemy = new Enemy(enemyType); enemy.x = Math.random() * 1688 + 180; enemy.y = -100; if (enemyType === 'golem') { golemCount++; } totalEnemyCount++; enemies.push(enemy); game.addChild(enemy); } else if (spawnPattern === 'behind') { // Spawn knight behind golems var enemy = new Enemy(enemyType); enemy.x = Math.random() * 1688 + 180; enemy.y = -300; // Further back totalEnemyCount++; enemies.push(enemy); game.addChild(enemy); } else if (spawnPattern === 'back') { // Spawn witch at the back var enemy = new Enemy(enemyType); enemy.x = Math.random() * 1688 + 180; enemy.y = -500; // Much further back if (enemyType === 'witch') { witchCount++; // Spawn 2 skeletons with 25 health each in front of witch for (var s = 0; s < 2; s++) { var skeleton = new Enemy('skeleton'); skeleton.health = 25; skeleton.maxHealth = 25; skeleton.children[1].setText(skeleton.health.toString()); // Update health display skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch skeleton.y = enemy.y + 100; // Position skeletons in front of witch skeleton.createdBy = enemy; // Link skeleton to witch enemies.push(skeleton); game.addChild(skeleton); enemy.skeletonCount++; enemy.spawnedSkeletons.push(skeleton); totalEnemyCount++; } } totalEnemyCount++; enemies.push(enemy); game.addChild(enemy); } } function handleMove(x, y, obj) { if (draggedDie) { draggedDie.x = x; draggedDie.y = y; } } function handleUp(x, y, obj) { if (draggedDie) { var merged = false; var trashed = false; var moved = false; // Check if dropped on trash area if (draggedDie.intersects(trashArea)) { // Destroy die and give 5 gold gold += 5; goldText.setText('Gold: ' + gold); // Remove die from slot var slot = diceSlots[draggedDie.slotIndex]; slot.isEmpty = true; slot.occupiedBy = null; // Remove corresponding defender for (var i = defenders.length - 1; i >= 0; i--) { if (defenders[i].linkedDie === draggedDie) { defenders[i].destroy(); defenders.splice(i, 1); break; } } // Remove die draggedDie.destroy(); dice.splice(dice.indexOf(draggedDie), 1); trashed = true; } if (!trashed) { // Check if dropped on an empty slot for (var i = 0; i < diceSlots.length; i++) { var targetSlot = diceSlots[i]; if (targetSlot.isEmpty && draggedDie.intersects(targetSlot)) { // Move die to new slot var oldSlot = diceSlots[draggedDie.slotIndex]; oldSlot.isEmpty = true; oldSlot.occupiedBy = null; // Update new slot targetSlot.isEmpty = false; targetSlot.occupiedBy = draggedDie; draggedDie.slotIndex = targetSlot.gridIndex; draggedDie.x = targetSlot.x; draggedDie.y = targetSlot.y; draggedDie.originalPosition.x = targetSlot.x; draggedDie.originalPosition.y = targetSlot.y; // Move corresponding defender to new wall position for (var j = 0; j < defenders.length; j++) { if (defenders[j].linkedDie === draggedDie) { var newWallSlot = wallSlots[draggedDie.slotIndex % 9]; defenders[j].x = newWallSlot.x; defenders[j].y = newWallSlot.y; break; } } moved = true; break; } } } if (!trashed && !moved) { // Check for merge with other dice for (var i = 0; i < dice.length; i++) { var otherDie = dice[i]; if (otherDie !== draggedDie && otherDie.type === draggedDie.type && otherDie.level === draggedDie.level && draggedDie.intersects(otherDie)) { // Merge dice mergeDice(draggedDie, otherDie); merged = true; break; } } if (!merged) { // Return to original position draggedDie.x = draggedDie.originalPosition.x; draggedDie.y = draggedDie.originalPosition.y; } } draggedDie = null; } } function mergeDice(die1, die2) { // Remove die1 from game var slot1 = diceSlots[die1.slotIndex]; slot1.isEmpty = true; slot1.occupiedBy = null; // Remove corresponding defender for (var i = defenders.length - 1; i >= 0; i--) { if (defenders[i].linkedDie === die1) { defenders[i].destroy(); defenders.splice(i, 1); break; } } die1.destroy(); dice.splice(dice.indexOf(die1), 1); // Upgrade die2 die2.updateLevel(die2.level + 1); // Update corresponding defender for (var i = 0; i < defenders.length; i++) { if (defenders[i].linkedDie === die2) { defenders[i].level = die2.level; break; } } LK.getSound('merge').play(); } game.move = handleMove; game.up = handleUp; game.update = function () { // Update game timer gameTimer++; // Update stage indicator var currentStage = 1; if (gameTimer > 12600) { currentStage = 5; } else if (gameTimer > 9900) { currentStage = 4; } else if (gameTimer > 7200) { currentStage = 3; } else if (gameTimer > 3600) { currentStage = 2; } stageText.setText('Stage: ' + currentStage); // Progressive enemy spawning enemySpawnTimer++; var currentInterval = enemySpawnInterval; // Calculate progressive spawn rate increase var baseInterval = 600; // Start at 10 seconds var minInterval = 60; // Minimum 1 second spawn rate var reductionRate = gameTimer * 0.0001; // Gradual reduction over time currentInterval = Math.max(minInterval, baseInterval - gameTimer * 0.02); // Stage-based spawn intervals // Stage 1: Slow spawning (0-60 seconds) - only trolls if (gameTimer <= 3600) { currentInterval = 200; // 3.33 seconds (reduced spawn rate) } // Stage 2: Moderate spawning (60-120 seconds) - trolls + skeletons else if (gameTimer <= 7200) { currentInterval = 105; // 1.75 seconds (reduced from 2.5) } // Stage 3: Fast spawning (120-165 seconds) - trolls + knights else if (gameTimer <= 9900) { currentInterval = 75; // 1.25 seconds (reduced from 1.5) } // Stage 4: Very fast spawning (165-210 seconds) - knights + witches + trolls else if (gameTimer <= 12600) { currentInterval = 45; // 0.75 seconds (reduced from 1) } // Stage 5: Maximum spawning (210+ seconds) - golems, knights, witches formation else { currentInterval = 30; // 0.5 seconds (reduced from 0.75) } if (enemySpawnTimer >= currentInterval) { enemySpawnTimer = 0; spawnEnemy(); } // Update bullets and check collisions for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; // Check if bullet should be destroyed if (bullet.shouldDestroy || bullet.y < -50 || bullet.y > 2732 || bullet.x < 0 || bullet.x > 2048) { bullet.destroy(); bullets.splice(i, 1); continue; } // Check collision with target or any enemy var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (bullet.intersects(enemy)) { // Handle area damage for cannon and dark mage if (bullet.bulletType === 'cannonBall') { // Small area damage for cannon var areaDamage = new AreaDamage(enemy.x, enemy.y, 80, bullet.damage, enemy); areaDamage.lifetime = 120; // 2 seconds at 60fps game.addChild(areaDamage); } else if (bullet.bulletType === 'darkMagic') { // Large area damage for dark mage var areaDamage = new AreaDamage(enemy.x, enemy.y, 150, bullet.damage, enemy); areaDamage.lifetime = 120; // 2 seconds at 60fps game.addChild(areaDamage); } enemy.takeDamage(bullet.damage); var enemyDied = enemy.health <= 0; if (enemyDied) { // If this is a skeleton created by a witch, update witch's count if (enemy.type === 'skeleton' && enemy.createdBy) { enemy.createdBy.skeletonCount--; } // If this is a witch, decrement witch count if (enemy.type === 'witch') { witchCount--; } // If this is a golem, decrement golem count if (enemy.type === 'golem') { golemCount--; } // Decrement total enemy count totalEnemyCount--; enemy.destroy(); enemies.splice(j, 1); } // Special behavior for blueMagic - continue to next enemy if (bullet.bulletType === 'blueMagic') { // Initialize hit count if not set if (!bullet.hitCount) bullet.hitCount = 0; bullet.hitCount++; // Find next enemy to target (if this is the first hit) if (bullet.hitCount === 1) { var nextTarget = null; var shortestDistance = Infinity; for (var k = 0; k < enemies.length; k++) { var nextEnemy = enemies[k]; if (nextEnemy !== enemy && nextEnemy.parent) { var dx = nextEnemy.x - bullet.x; var dy = nextEnemy.y - bullet.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < shortestDistance) { shortestDistance = distance; nextTarget = nextEnemy; } } } // Set new target if found if (nextTarget) { bullet.target = nextTarget; } else { // No more targets, destroy bullet bullet.destroy(); bullets.splice(i, 1); } } else { // Second hit, destroy bullet bullet.destroy(); bullets.splice(i, 1); } } else { // Regular bullet behavior - only destroy if enemy survived if (!enemyDied) { bullet.destroy(); bullets.splice(i, 1); } } hitEnemy = true; break; } } } // Update and cleanup area damage objects for (var i = game.children.length - 1; i >= 0; i--) { var child = game.children[i]; if (child.shouldDestroy) { child.destroy(); } } // Update ranged projectiles for (var i = rangedProjectiles.length - 1; i >= 0; i--) { var projectile = rangedProjectiles[i]; if (projectile.shouldDestroy || projectile.y > 2000) { projectile.destroy(); rangedProjectiles.splice(i, 1); } } // Check if enemies reached the wall (only for melee enemies) for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; if (enemy.behavior !== 'ranged' && enemy.y >= 1770 && !enemy.hasAttackedWall) { // Enemy attacks wall - reduce health and update display enemy.attackWall(); // If this is a skeleton created by a witch, update witch's count if (enemy.type === 'skeleton' && enemy.createdBy) { enemy.createdBy.skeletonCount--; } // If this is a witch, decrement witch count if (enemy.type === 'witch') { witchCount--; } // If this is a golem, decrement golem count if (enemy.type === 'golem') { golemCount--; } // Decrement total enemy count totalEnemyCount--; // Remove enemy after attacking wall enemy.destroy(); enemies.splice(i, 1); } } }; // Start with one free die buyDie();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AreaDamage = Container.expand(function (centerX, centerY, radius, damage, excludeEnemy) {
var self = Container.call(this);
self.x = centerX;
self.y = centerY;
self.radius = radius;
self.damage = damage;
self.excludeEnemy = excludeEnemy;
self.lifetime = 5; // Frames to exist
// Create visual effect
var assetName = radius > 100 ? 'largeAreaDamage' : 'smallAreaDamage';
var visualEffect = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
visualEffect.alpha = 0.6;
// Animate the visual effect
tween(visualEffect, {
alpha: 0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 2000,
easing: tween.easeOut
});
self.update = function () {
// Deal damage to all enemies in radius
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy === self.excludeEnemy) continue; // Skip the original target
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
enemy.takeDamage(self.damage);
// If enemy health becomes negative, destroy immediately
if (enemy.health <= 0) {
enemy.die();
enemy.destroy();
for (var j = enemies.length - 1; j >= 0; j--) {
if (enemies[j] === enemy) {
enemies.splice(j, 1);
break;
}
}
}
}
}
self.lifetime--;
if (self.lifetime <= 0) {
self.shouldDestroy = true;
}
};
return self;
});
var Bullet = Container.expand(function (startX, startY, damage, target, bulletType) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.speed = 8;
self.damage = damage || 1;
self.target = target;
self.bulletType = bulletType || 'bullet';
var bulletGraphics = self.attachAsset(self.bulletType, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (self.target && self.target.parent) {
// Calculate direction to target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Move towards target
var directionX = dx / distance;
var directionY = dy / distance;
self.x += directionX * self.speed;
self.y += directionY * self.speed;
// Store direction for straight line movement
self.directionX = directionX;
self.directionY = directionY;
} else {
// Close enough to target
if (self.target.takeDamage) {
self.target.takeDamage(self.damage);
}
self.shouldDestroy = true;
}
} else {
// Target is gone, continue in straight line
if (self.directionX !== undefined && self.directionY !== undefined) {
self.x += self.directionX * self.speed;
self.y += self.directionY * self.speed;
} else {
// No direction set, destroy bullet
self.shouldDestroy = true;
}
}
};
return self;
});
var Defender = Container.expand(function (type, level) {
var self = Container.call(this);
self.type = type || 'green';
self.level = level || 1;
self.shootTimer = 0;
self.shootInterval = 60; // 1 second at 60fps
self.damage = 1;
var assetNames = {
'green': 'archer',
'yellow': 'spearFighter',
'red': 'musketeer',
'orange': 'cannon',
'blue': 'skyMage',
'black': 'darkMage'
};
var defenderGraphics = self.attachAsset(assetNames[self.type], {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Check if linked die just finished rolling
if (self.linkedDie && self.linkedDie.justFinishedRolling) {
self.shoot();
}
};
self.shoot = function () {
var die = self.linkedDie;
if (die && enemies.length > 0 && die.justFinishedRolling) {
// Find nearest enemy
var nearestEnemy = null;
var shortestDistance = Infinity;
for (var e = 0; e < enemies.length; e++) {
var enemy = enemies[e];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < shortestDistance) {
shortestDistance = distance;
nearestEnemy = enemy;
}
}
if (nearestEnemy) {
var shots = die.getRollValue();
var bulletTypes = {
'green': 'arrow',
'yellow': 'spear',
'red': 'musketBall',
'orange': 'cannonBall',
'blue': 'blueMagic',
'black': 'darkMagic'
};
var bulletType = bulletTypes[self.type] || 'bullet';
// Set damage based on defender type
var bulletDamage = self.damage;
if (self.type === 'red') {
bulletDamage = 2; // Musketeer deals 2 damage
} else if (self.type === 'blue') {
bulletDamage = 2; // Sky mage deals 2 damage
}
// Fire bullets in sequence with 0.3 second intervals
for (var i = 0; i < shots; i++) {
LK.setTimeout(function (shotIndex) {
return function () {
// Find target again (in case original target was destroyed)
var currentTarget = nearestEnemy;
if (!currentTarget || !currentTarget.parent) {
// Find new nearest enemy
for (var e = 0; e < enemies.length; e++) {
var enemy = enemies[e];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (!currentTarget || distance < Math.sqrt((currentTarget.x - self.x) * (currentTarget.x - self.x) + (currentTarget.y - self.y) * (currentTarget.y - self.y))) {
currentTarget = enemy;
}
}
}
if (currentTarget && currentTarget.parent) {
var bullet = new Bullet(self.x, self.y - 40, bulletDamage, currentTarget, bulletType);
bullets.push(bullet);
game.addChild(bullet);
LK.getSound('shoot').play();
}
};
}(i), i * 300); // 300ms = 0.3 seconds
}
die.justFinishedRolling = false;
}
}
};
return self;
});
var Die = Container.expand(function (type, level) {
var self = Container.call(this);
self.type = type || 'green';
self.level = level || 1;
self.rollTimer = 0;
self.rollInterval = 180; // 3 seconds at 60fps
self.lastRoll = 1;
self.isDragging = false;
self.justFinishedRolling = false;
self.originalPosition = {
x: 0,
y: 0
};
var assetNames = {
'green': 'greenDie',
'yellow': 'yellowDie',
'red': 'redDie',
'orange': 'orangeDie',
'blue': 'blueDie',
'black': 'blackDie'
};
var dieGraphics = self.attachAsset(assetNames[self.type], {
anchorX: 0.5,
anchorY: 0.5
});
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
var levelText = new Text2(self.level.toString(), {
size: 40,
fill: textColor
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 0;
levelText.y = -20;
self.addChild(levelText);
var rollText = new Text2(self.lastRoll.toString(), {
size: 60,
fill: textColor
});
rollText.anchor.set(0.5, 0.5);
rollText.x = 0;
rollText.y = 20;
self.addChild(rollText);
self.down = function (x, y, obj) {
self.isDragging = true;
self.originalPosition.x = self.x;
self.originalPosition.y = self.y;
draggedDie = self;
};
self.update = function () {
self.rollTimer++;
// Blue dice rotate every 2 seconds (120 ticks), others every 3 seconds (180 ticks)
var currentInterval = self.rollInterval;
if (self.type === 'blue') {
currentInterval = 120; // 2 seconds for blue dice
}
if (self.rollTimer >= currentInterval) {
self.rollTimer = 0;
// Unfair dice roll - higher probability for lower values
var rollWeights = [35, 25, 20, 12, 6, 2]; // Weights for 1,2,3,4,5,6
var totalWeight = 0;
for (var w = 0; w < rollWeights.length; w++) {
totalWeight += rollWeights[w];
}
var random = Math.random() * totalWeight;
var currentWeight = 0;
self.lastRoll = 1; // Default to 1
for (var w = 0; w < rollWeights.length; w++) {
currentWeight += rollWeights[w];
if (random <= currentWeight) {
self.lastRoll = w + 1;
break;
}
}
var displayValue = self.lastRoll + (self.level - 1) * 2;
rollText.setText(displayValue.toString());
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
rollText.fill = textColor;
self.justFinishedRolling = true;
// Add rolling animation
tween(dieGraphics, {
rotation: Math.PI * 2
}, {
duration: 500,
easing: tween.easeOut
});
}
};
self.getRollValue = function () {
return self.lastRoll + (self.level - 1) * 2;
};
self.updateLevel = function (newLevel) {
self.level = newLevel;
levelText.setText(self.level.toString());
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
levelText.fill = textColor;
rollText.fill = textColor;
};
return self;
});
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'troll';
// Set stats based on enemy type
var enemyStats = {
'troll': {
health: 5,
speed: 1,
goldValue: Math.floor(Math.random() * 8) + 5,
asset: 'troll',
behavior: 'melee'
},
'skeleton': {
health: 10,
speed: 1.5,
goldValue: Math.floor(Math.random() * 11) + 8,
asset: 'skeleton',
behavior: 'melee'
},
'knight': {
health: 1,
speed: 3,
goldValue: Math.floor(Math.random() * 5) + 3,
asset: 'knight',
behavior: 'melee'
},
'witch': {
health: 30,
speed: 0.5,
goldValue: Math.floor(Math.random() * 15) + 12,
asset: 'witch',
behavior: 'summoner'
},
'golem': {
health: 100,
speed: 0.8,
goldValue: Math.floor(Math.random() * 23) + 23,
asset: 'golem',
behavior: 'melee'
},
'demon': {
health: 75,
speed: 1.2,
goldValue: Math.floor(Math.random() * 18) + 15,
asset: 'demon',
behavior: 'ranged'
}
};
var stats = enemyStats[self.type] || enemyStats['troll'];
self.health = stats.health;
self.maxHealth = stats.health;
self.speed = stats.speed;
self.goldValue = stats.goldValue;
self.behavior = stats.behavior;
self.summonTimer = 0;
self.attackTimer = 0;
self.hasAttackedWall = false;
self.skeletonCount = 0; // Track how many skeletons this witch has created
self.spawnedSkeletons = []; // Track spawned skeletons for cleanup
var enemyGraphics = self.attachAsset(stats.asset, {
anchorX: 0.5,
anchorY: 0.5
});
var healthBar = new Text2(self.health.toString(), {
size: 30,
fill: 0xFF0000
});
healthBar.anchor.set(0.5, 0.5);
healthBar.x = 0;
healthBar.y = -80;
self.addChild(healthBar);
self.update = function () {
if (self.behavior === 'ranged' && self.y >= 1500) {
// Demon ranged attack behavior - stop and shoot
self.attackTimer++;
if (self.attackTimer >= 120) {
// Every 2 seconds
self.attackTimer = 0;
self.rangedAttack();
}
} else if (self.behavior === 'summoner') {
// Witch behavior - move slowly (no longer summons skeletons)
if (self.y < 1770) {
self.y += self.speed;
}
} else if (self.y >= 1770 && !self.hasAttackedWall) {
// Reached wall - deal damage
self.hasAttackedWall = true;
self.attackWall();
} else if (self.y < 1770) {
// Normal movement
self.y += self.speed;
}
};
self.rangedAttack = function () {
if (wallHealth > 0) {
var projectile = new RangedProjectile(self.x, self.y, 1770);
rangedProjectiles.push(projectile);
game.addChild(projectile);
}
};
self.summonSkeleton = function () {
// Check if we already have 3 skeletons
if (self.skeletonCount >= 3) {
return; // Don't spawn more skeletons
}
var skeleton = new Enemy('skeleton');
skeleton.x = self.x + (Math.random() - 0.5) * 100;
skeleton.y = self.y + 50;
skeleton.createdBy = self; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
self.skeletonCount++;
self.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
};
self.attackWall = function () {
wallHealth -= 1;
wallHealthText.setText('Wall HP: ' + wallHealth);
LK.getSound('wallHit').play();
LK.effects.flashObject(wall, 0xFF0000, 300);
if (wallHealth <= 0) {
// Game over
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
};
self.takeDamage = function (damage) {
self.health -= damage;
healthBar.setText(self.health.toString());
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
gold += self.goldValue;
goldText.setText('Gold: ' + gold);
LK.getSound('enemyDie').play();
LK.effects.flashObject(self, 0xFF0000, 500);
// If this is a witch, clean up references to spawned skeletons and decrement witch count
if (self.type === 'witch') {
witchCount--;
for (var i = 0; i < self.spawnedSkeletons.length; i++) {
var skeleton = self.spawnedSkeletons[i];
if (skeleton && skeleton.createdBy === self) {
skeleton.createdBy = null;
}
}
}
// Decrement golem count if this is a golem
if (self.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
};
return self;
});
var RangedProjectile = Container.expand(function (startX, startY, targetY) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.targetY = targetY;
self.speed = 4;
var projectileGraphics = self.attachAsset('rangedAttack', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.y += self.speed;
if (self.y >= self.targetY) {
// Hit the wall
wallHealth -= 1; // Each enemy deals 1 damage to wall
wallHealthText.setText('Wall HP: ' + wallHealth);
LK.getSound('wallHit').play();
LK.effects.flashObject(wall, 0xFF0000, 300);
self.shouldDestroy = true;
if (wallHealth <= 0) {
// Game over
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F5233
});
/****
* Game Code
****/
// Add fantasy world background image
var fantasyBackground = game.addChild(LK.getAsset('fantasyWorldBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Game variables
var gold = 10;
var enemies = [];
var bullets = [];
var dice = [];
var defenders = [];
var wallSlots = [];
var diceSlots = [];
var draggedDie = null;
var trashArea = null;
var enemySpawnTimer = 0;
var enemySpawnInterval = 300; // 5 seconds initially
var gameTimer = 0; // Track game time in ticks
var dicesPurchased = 0; // Track number of dice purchased for pricing
var wallHealth = 10;
var rangedProjectiles = [];
var witchCount = 0; // Track number of witches currently on field
var golemCount = 0; // Track number of golems currently on field
var totalEnemyCount = 0; // Track total number of enemies on field
// UI elements
var goldText = new Text2('Gold: 10', {
size: 60,
fill: 0xFFFF00
});
goldText.anchor.set(0, 0);
goldText.x = 150;
goldText.y = 50;
LK.gui.topLeft.addChild(goldText);
// Wall health display
var wallHealthText = new Text2('Wall HP: 10', {
size: 50,
fill: 0xFF0000
});
wallHealthText.anchor.set(0, 0);
wallHealthText.x = 150;
wallHealthText.y = 120;
LK.gui.topLeft.addChild(wallHealthText);
// Stage indicator display
var stageText = new Text2('Stage: 1', {
size: 50,
fill: 0x00FF00
});
stageText.anchor.set(1, 0);
stageText.x = -50;
stageText.y = 50;
LK.gui.topRight.addChild(stageText);
// Create wall
var wall = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 1,
x: 1024,
y: 1900
}));
// Create wall slots (9 defensive positions)
for (var i = 0; i < 9; i++) {
var slot = game.addChild(LK.getAsset('wallSlot', {
anchorX: 0.5,
anchorY: 0.5,
x: 200 + i * 200,
y: 1900 - 75
}));
wallSlots.push(slot);
}
// Create dice grid (3x3)
var diceGridStartX = 824; // Center on screen (1024 - 200)
var diceGridStartY = 2100;
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var slot = game.addChild(LK.getAsset('diceSlot', {
anchorX: 0.5,
anchorY: 0.5,
x: diceGridStartX + col * 200,
y: diceGridStartY + row * 200
}));
slot.isEmpty = true;
slot.gridIndex = row * 3 + col;
diceSlots.push(slot);
}
}
// Create trash area to the left of dice grid
trashArea = game.addChild(LK.getAsset('trashArea', {
anchorX: 0.5,
anchorY: 0.5,
x: 524,
// Left of dice grid
y: 2200
}));
// Buy button
var buyButton = game.addChild(LK.getAsset('buyButton', {
anchorX: 0.5,
anchorY: 0.5,
x: 1600,
y: 2200
}));
// Price display text
var priceText = new Text2('Price: 10', {
size: 40,
fill: 0xFFFFFF
});
priceText.anchor.set(0.5, 0);
priceText.x = 1600;
priceText.y = 2400;
game.addChild(priceText);
buyButton.down = function (x, y, obj) {
var currentPrice = 10 + dicesPurchased * 5;
if (gold >= currentPrice) {
buyDie();
}
};
function buyDie() {
var emptySlot = null;
for (var i = 0; i < diceSlots.length; i++) {
if (diceSlots[i].isEmpty) {
emptySlot = diceSlots[i];
break;
}
}
if (emptySlot) {
var currentPrice = 10 + dicesPurchased * 5;
gold -= currentPrice;
dicesPurchased++;
goldText.setText('Gold: ' + gold);
// Update price display for next purchase
var nextPrice = 10 + dicesPurchased * 5;
priceText.setText('Price: ' + nextPrice);
var dieTypes = ['green', 'yellow', 'red', 'orange', 'blue', 'black'];
var weights = [30, 30, 20, 20, 10, 10]; // Common, Common, Rare, Rare, Legendary, Legendary
var randomType = getWeightedRandom(dieTypes, weights);
var newDie = new Die(randomType, 1);
newDie.x = emptySlot.x;
newDie.y = emptySlot.y;
newDie.slotIndex = emptySlot.gridIndex;
dice.push(newDie);
game.addChild(newDie);
emptySlot.isEmpty = false;
emptySlot.occupiedBy = newDie;
// Create corresponding defender
createDefender(newDie);
}
}
function getWeightedRandom(items, weights) {
var totalWeight = 0;
for (var i = 0; i < weights.length; i++) {
totalWeight += weights[i];
}
var random = Math.random() * totalWeight;
var currentWeight = 0;
for (var i = 0; i < items.length; i++) {
currentWeight += weights[i];
if (random <= currentWeight) {
return items[i];
}
}
return items[0];
}
function createDefender(die) {
var defender = new Defender(die.type, die.level);
var wallSlot = wallSlots[die.slotIndex % 9];
defender.x = wallSlot.x;
defender.y = wallSlot.y;
defender.linkedDie = die;
defenders.push(defender);
game.addChild(defender);
}
function spawnEnemy() {
// Check total enemy limit
if (totalEnemyCount >= 20) {
return; // Don't spawn if at maximum enemy count
}
var enemyType = 'troll';
var enemiesToSpawn = 1;
var spawnPattern = 'single';
// Stage 1: Only trolls (0-60 seconds) - slow spawn rate
if (gameTimer <= 3600) {
enemyType = 'troll';
spawnPattern = 'single';
}
// Stage 2: Trolls + skeletons (60-120 seconds) - faster spawn rate
else if (gameTimer <= 7200) {
enemyType = Math.random() < 0.6 ? 'troll' : 'skeleton';
spawnPattern = 'single';
}
// Stage 3: Trolls + knights (120-165 seconds) - trolls get more health
else if (gameTimer <= 9900) {
enemyType = Math.random() < 0.7 ? 'troll' : 'knight';
spawnPattern = 'single';
}
// Stage 4: Knights + witches + trolls (165-210 seconds)
else if (gameTimer <= 12600) {
var rand = Math.random();
if (rand < 0.4) enemyType = 'knight';else if (rand < 0.7 && witchCount < 4) enemyType = 'witch';else enemyType = 'troll';
spawnPattern = 'single';
}
// Stage 5: Golems in front, then knights behind, then witches (210+ seconds)
else {
var rand = Math.random();
if (rand < 0.3 && golemCount < 3) {
enemyType = 'golem';
spawnPattern = 'front';
} else if (rand < 0.7) {
enemyType = 'knight';
spawnPattern = 'behind';
} else if (witchCount < 4) {
enemyType = 'witch';
spawnPattern = 'back';
} else {
enemyType = 'knight';
spawnPattern = 'behind';
}
}
// Spawn enemies based on pattern
if (spawnPattern === 'single') {
var enemy = new Enemy(enemyType);
// Stage 3+: Give trolls and skeletons more health
if (gameTimer > 7200) {
if (enemyType === 'troll') {
enemy.health = 10;
enemy.maxHealth = 10;
enemy.children[1].setText(enemy.health.toString()); // Update health display
} else if (enemyType === 'skeleton') {
enemy.health = 15;
enemy.maxHealth = 15;
enemy.children[1].setText(enemy.health.toString()); // Update health display
}
}
enemy.x = Math.random() * 1688 + 180; // Random X position (avoiding 80px margins)
enemy.y = -100; // Start above screen
if (enemyType === 'witch') {
witchCount++;
// Spawn 2 skeletons with 25 health each in front of witch
for (var s = 0; s < 2; s++) {
var skeleton = new Enemy('skeleton');
skeleton.health = 25;
skeleton.maxHealth = 25;
skeleton.children[1].setText(skeleton.health.toString()); // Update health display
skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch
skeleton.y = enemy.y + 100; // Position skeletons in front of witch
skeleton.createdBy = enemy; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
enemy.skeletonCount++;
enemy.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
}
}
if (enemyType === 'golem') {
golemCount++;
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'front') {
// Spawn golem at front
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -100;
if (enemyType === 'golem') {
golemCount++;
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'behind') {
// Spawn knight behind golems
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -300; // Further back
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'back') {
// Spawn witch at the back
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -500; // Much further back
if (enemyType === 'witch') {
witchCount++;
// Spawn 2 skeletons with 25 health each in front of witch
for (var s = 0; s < 2; s++) {
var skeleton = new Enemy('skeleton');
skeleton.health = 25;
skeleton.maxHealth = 25;
skeleton.children[1].setText(skeleton.health.toString()); // Update health display
skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch
skeleton.y = enemy.y + 100; // Position skeletons in front of witch
skeleton.createdBy = enemy; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
enemy.skeletonCount++;
enemy.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
}
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
}
}
function handleMove(x, y, obj) {
if (draggedDie) {
draggedDie.x = x;
draggedDie.y = y;
}
}
function handleUp(x, y, obj) {
if (draggedDie) {
var merged = false;
var trashed = false;
var moved = false;
// Check if dropped on trash area
if (draggedDie.intersects(trashArea)) {
// Destroy die and give 5 gold
gold += 5;
goldText.setText('Gold: ' + gold);
// Remove die from slot
var slot = diceSlots[draggedDie.slotIndex];
slot.isEmpty = true;
slot.occupiedBy = null;
// Remove corresponding defender
for (var i = defenders.length - 1; i >= 0; i--) {
if (defenders[i].linkedDie === draggedDie) {
defenders[i].destroy();
defenders.splice(i, 1);
break;
}
}
// Remove die
draggedDie.destroy();
dice.splice(dice.indexOf(draggedDie), 1);
trashed = true;
}
if (!trashed) {
// Check if dropped on an empty slot
for (var i = 0; i < diceSlots.length; i++) {
var targetSlot = diceSlots[i];
if (targetSlot.isEmpty && draggedDie.intersects(targetSlot)) {
// Move die to new slot
var oldSlot = diceSlots[draggedDie.slotIndex];
oldSlot.isEmpty = true;
oldSlot.occupiedBy = null;
// Update new slot
targetSlot.isEmpty = false;
targetSlot.occupiedBy = draggedDie;
draggedDie.slotIndex = targetSlot.gridIndex;
draggedDie.x = targetSlot.x;
draggedDie.y = targetSlot.y;
draggedDie.originalPosition.x = targetSlot.x;
draggedDie.originalPosition.y = targetSlot.y;
// Move corresponding defender to new wall position
for (var j = 0; j < defenders.length; j++) {
if (defenders[j].linkedDie === draggedDie) {
var newWallSlot = wallSlots[draggedDie.slotIndex % 9];
defenders[j].x = newWallSlot.x;
defenders[j].y = newWallSlot.y;
break;
}
}
moved = true;
break;
}
}
}
if (!trashed && !moved) {
// Check for merge with other dice
for (var i = 0; i < dice.length; i++) {
var otherDie = dice[i];
if (otherDie !== draggedDie && otherDie.type === draggedDie.type && otherDie.level === draggedDie.level && draggedDie.intersects(otherDie)) {
// Merge dice
mergeDice(draggedDie, otherDie);
merged = true;
break;
}
}
if (!merged) {
// Return to original position
draggedDie.x = draggedDie.originalPosition.x;
draggedDie.y = draggedDie.originalPosition.y;
}
}
draggedDie = null;
}
}
function mergeDice(die1, die2) {
// Remove die1 from game
var slot1 = diceSlots[die1.slotIndex];
slot1.isEmpty = true;
slot1.occupiedBy = null;
// Remove corresponding defender
for (var i = defenders.length - 1; i >= 0; i--) {
if (defenders[i].linkedDie === die1) {
defenders[i].destroy();
defenders.splice(i, 1);
break;
}
}
die1.destroy();
dice.splice(dice.indexOf(die1), 1);
// Upgrade die2
die2.updateLevel(die2.level + 1);
// Update corresponding defender
for (var i = 0; i < defenders.length; i++) {
if (defenders[i].linkedDie === die2) {
defenders[i].level = die2.level;
break;
}
}
LK.getSound('merge').play();
}
game.move = handleMove;
game.up = handleUp;
game.update = function () {
// Update game timer
gameTimer++;
// Update stage indicator
var currentStage = 1;
if (gameTimer > 12600) {
currentStage = 5;
} else if (gameTimer > 9900) {
currentStage = 4;
} else if (gameTimer > 7200) {
currentStage = 3;
} else if (gameTimer > 3600) {
currentStage = 2;
}
stageText.setText('Stage: ' + currentStage);
// Progressive enemy spawning
enemySpawnTimer++;
var currentInterval = enemySpawnInterval;
// Calculate progressive spawn rate increase
var baseInterval = 600; // Start at 10 seconds
var minInterval = 60; // Minimum 1 second spawn rate
var reductionRate = gameTimer * 0.0001; // Gradual reduction over time
currentInterval = Math.max(minInterval, baseInterval - gameTimer * 0.02);
// Stage-based spawn intervals
// Stage 1: Slow spawning (0-60 seconds) - only trolls
if (gameTimer <= 3600) {
currentInterval = 200; // 3.33 seconds (reduced spawn rate)
}
// Stage 2: Moderate spawning (60-120 seconds) - trolls + skeletons
else if (gameTimer <= 7200) {
currentInterval = 105; // 1.75 seconds (reduced from 2.5)
}
// Stage 3: Fast spawning (120-165 seconds) - trolls + knights
else if (gameTimer <= 9900) {
currentInterval = 75; // 1.25 seconds (reduced from 1.5)
}
// Stage 4: Very fast spawning (165-210 seconds) - knights + witches + trolls
else if (gameTimer <= 12600) {
currentInterval = 45; // 0.75 seconds (reduced from 1)
}
// Stage 5: Maximum spawning (210+ seconds) - golems, knights, witches formation
else {
currentInterval = 30; // 0.5 seconds (reduced from 0.75)
}
if (enemySpawnTimer >= currentInterval) {
enemySpawnTimer = 0;
spawnEnemy();
}
// Update bullets and check collisions
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Check if bullet should be destroyed
if (bullet.shouldDestroy || bullet.y < -50 || bullet.y > 2732 || bullet.x < 0 || bullet.x > 2048) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with target or any enemy
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (bullet.intersects(enemy)) {
// Handle area damage for cannon and dark mage
if (bullet.bulletType === 'cannonBall') {
// Small area damage for cannon
var areaDamage = new AreaDamage(enemy.x, enemy.y, 80, bullet.damage, enemy);
areaDamage.lifetime = 120; // 2 seconds at 60fps
game.addChild(areaDamage);
} else if (bullet.bulletType === 'darkMagic') {
// Large area damage for dark mage
var areaDamage = new AreaDamage(enemy.x, enemy.y, 150, bullet.damage, enemy);
areaDamage.lifetime = 120; // 2 seconds at 60fps
game.addChild(areaDamage);
}
enemy.takeDamage(bullet.damage);
var enemyDied = enemy.health <= 0;
if (enemyDied) {
// If this is a skeleton created by a witch, update witch's count
if (enemy.type === 'skeleton' && enemy.createdBy) {
enemy.createdBy.skeletonCount--;
}
// If this is a witch, decrement witch count
if (enemy.type === 'witch') {
witchCount--;
}
// If this is a golem, decrement golem count
if (enemy.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
enemy.destroy();
enemies.splice(j, 1);
}
// Special behavior for blueMagic - continue to next enemy
if (bullet.bulletType === 'blueMagic') {
// Initialize hit count if not set
if (!bullet.hitCount) bullet.hitCount = 0;
bullet.hitCount++;
// Find next enemy to target (if this is the first hit)
if (bullet.hitCount === 1) {
var nextTarget = null;
var shortestDistance = Infinity;
for (var k = 0; k < enemies.length; k++) {
var nextEnemy = enemies[k];
if (nextEnemy !== enemy && nextEnemy.parent) {
var dx = nextEnemy.x - bullet.x;
var dy = nextEnemy.y - bullet.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < shortestDistance) {
shortestDistance = distance;
nextTarget = nextEnemy;
}
}
}
// Set new target if found
if (nextTarget) {
bullet.target = nextTarget;
} else {
// No more targets, destroy bullet
bullet.destroy();
bullets.splice(i, 1);
}
} else {
// Second hit, destroy bullet
bullet.destroy();
bullets.splice(i, 1);
}
} else {
// Regular bullet behavior - only destroy if enemy survived
if (!enemyDied) {
bullet.destroy();
bullets.splice(i, 1);
}
}
hitEnemy = true;
break;
}
}
}
// Update and cleanup area damage objects
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child.shouldDestroy) {
child.destroy();
}
}
// Update ranged projectiles
for (var i = rangedProjectiles.length - 1; i >= 0; i--) {
var projectile = rangedProjectiles[i];
if (projectile.shouldDestroy || projectile.y > 2000) {
projectile.destroy();
rangedProjectiles.splice(i, 1);
}
}
// Check if enemies reached the wall (only for melee enemies)
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.behavior !== 'ranged' && enemy.y >= 1770 && !enemy.hasAttackedWall) {
// Enemy attacks wall - reduce health and update display
enemy.attackWall();
// If this is a skeleton created by a witch, update witch's count
if (enemy.type === 'skeleton' && enemy.createdBy) {
enemy.createdBy.skeletonCount--;
}
// If this is a witch, decrement witch count
if (enemy.type === 'witch') {
witchCount--;
}
// If this is a golem, decrement golem count
if (enemy.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
// Remove enemy after attacking wall
enemy.destroy();
enemies.splice(i, 1);
}
}
};
// Start with one free die
buyDie();
ΧΧΧΧ Χ’Χ ΧΧ₯ ΧΧ§Χ©Χͺ ΧΧΧΧΧΧΧͺ. In-Game asset. 2d. High contrast. No shadows
ΧΧ₯ Χ©Χ Χ§Χ©Χͺ Χ€ΧΧ Χ ΧΧΧ€Χ ΧΧ’ΧΧ. In-Game asset. 2d. High contrast. No shadows
Blue circle magic attack. In-Game asset. 2d. High contrast. No shadows
Dice factory from the fantasy world. In-Game asset. 2d. High contrast. No shadows
Trash area for dice from fantasy. In-Game asset. 2d. High contrast. No shadows
Troll enemy. In-Game asset. 2d. High contrast. No shadows
Skeleton enemy. In-Game asset. 2d. High contrast. No shadows
Cannon from the fantasy. In-Game asset. 2d. High contrast. No shadows
From the bird view, top of the wall. In-Game asset. 2d. High contrast. No shadows
Χ’ΧΧΧΧ ΧΧ¦ΧΧ’ ΧΧΧΧ¨Χ Χ’Χ ΧΧΧΧ ΧΧΧΧ¨ΧΧͺ ΧΧ§ΧΧ€Χ ΧΧΧͺΧ ΧΧΧΧ ΧΧ ΧΧ ΧΧΧΧ’ΧΧ. In-Game asset. 2d. High contrast. No shadows
Χ§ΧΧ‘Χ ΧΧ€Χ ΧΧΧΧΧΧΧͺ. In-Game asset. 2d. High contrast. No shadows
Χ§ΧΧ‘Χ Χ©ΧΧΧ ΧΧΧΧ. In-Game asset. 2d. High contrast. No shadows
Black magic from fantasy. In-Game asset. 2d. High contrast. No shadows
ΧΧΧ©Χ€Χ ΧΧΧΧΧΧΧͺ. In-Game asset. 2d. High contrast. No shadows
Χ€Χ¨Χ© Χ¨ΧΧΧ Χ’Χ Χ‘ΧΧ‘. In-Game asset. 2d. High contrast. No shadows
ΧΧΧΧ Χ’Χ©ΧΧ ΧΧΧΧ ΧΧΧΧΧΧΧͺ. In-Game asset. 2d. High contrast. No shadows
Χ©ΧΧΧ ΧΧΧΧ ΧΧ’ΧΧ€Χ£. In-Game asset. 2d. High contrast. No shadows
ΧΧΧ‘Χ§ΧΧΧ¨ ΧΧΧΧΧΧΧͺ. In-Game asset. 2d. High contrast. No shadows
ΧΧΧΧ ΧΧ ΧΧͺ. In-Game asset. 2d. High contrast. No shadows
bullet. In-Game asset. 2d. High contrast. No shadows
Χ¨Χ§Χ’ ΧΧΧ©ΧΧ§ Χ€Χ ΧΧΧΧ ΧΧΧΧΧ ΧΧΧ©ΧΧΧ ΧΧΧ© Χ¦ΧΧΧ ΧΧΧ‘ΧΧ¨Χͺ ΧΧ Χ¨ΧΧ ΧΧΧ ΧΧΧ¨ΧΧ ΧΧΧΧΧͺ ΧΧΧ’Χ¨ΧΧͺ ΧΧΧΧ¦Χ’ ΧΧΧ ΧΧ©Χ Χ Χ©ΧΧΧ© ΧΧ’ΧΧΧΧ ΧΧ ΧΧΧ©ΧΧ ΧΧ¨ΧΧ§Χ ΧΧΧΧΧ. ΧΧ©ΧΧΧ© ΧΧͺΧΧͺΧΧ ΧΧ ΧΧΧΧ Χ Χ’Χ Χ§ Χ©Χ Χ¦Χ¨ΧΧ ΧΧΧΧ¨Χ ΧΧͺΧΧΧ Χ ΧΧΧΧ ΧΧΧΧ’ΧΧ ΧΧ§Χ ΧΧ ΧΧ, ΧΧ’ΧΧ£ ΧΧ¦ΧΧ€ΧΧ¨. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows