/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var BuildSpot = Container.expand(function () { var self = Container.call(this); // Override findTarget method so KnockKnock tower can detect enemies self.findTarget = function (enemies) { // Find closest enemy var closestEnemy = null; var closestDistance = self.range; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > 0) { var distance = self.distanceTo(enemy); if (distance <= self.range && (!closestEnemy || distance < closestDistance)) { closestEnemy = enemy; closestDistance = distance; } } } self.target = closestEnemy; return self.target; }; self.hasTower = false; self.towerType = null; // Create build spot sprite var spotSprite = self.attachAsset('buildSpot', { anchorX: 0.5, anchorY: 0.5 }); spotSprite.alpha = 0.5; self.down = function (x, y, obj) { if (self.hasTower) { return; } // Toggle build menu visibility if (buildMenu.visible && buildMenuSpot === self) { hideBuildMenu(); } else { showBuildMenu(self); } }; self.buildTower = function (type) { if (self.hasTower) { return false; } var towerCost = 10; if (type === 'pun') { towerCost = 10; } if (type === 'slapstick') { towerCost = 10; } if (type === 'knockknock') { towerCost = 10; } if (laughs >= towerCost) { // Deduct cost laughs -= towerCost; laughsText.setText("Laughs: " + laughs); // Create tower var tower; if (type === 'knockknock') { tower = new KnockKnockTower(); } else { tower = new Tower(); } tower.setup(type); tower.x = self.x; tower.y = self.y; game.addChild(tower); towers.push(tower); // Mark spot as used self.hasTower = true; self.towerType = type; spotSprite.alpha = 0.2; // Play build sound LK.getSound('buildTower').play(); // Hide build menu hideBuildMenu(); return true; } return false; }; return self; }); var Enemy = Container.expand(function () { var self = Container.call(this); // Properties self.health = 100; self.maxHealth = 100; self.speed = 2; self.value = 10; // Laughs earned when killed self.currentWaypointIndex = 0; self.reachedEnd = false; self.enemyType = 'normal'; // normal, punHater, slapstickHater self.healthBarWidth = 80; // Create enemy sprite var enemySprite = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); // Create health bar var healthBarBackground = LK.getAsset('path', { width: self.healthBarWidth, height: 10, color: 0x333333, anchorX: 0.5, anchorY: 0.5 }); healthBarBackground.y = -50; self.addChild(healthBarBackground); var healthBar = LK.getAsset('path', { width: self.healthBarWidth, height: 10, color: 0x00FF00, anchorX: 0, anchorY: 0.5 }); healthBar.x = -self.healthBarWidth / 2; healthBar.y = -50; self.addChild(healthBar); self.healthBar = healthBar; // Methods self.setup = function (type, difficulty) { self.enemyType = type; // Adjust properties based on type if (type === 'punHater') { enemySprite.tint = 0x3355FF; self.health = 70 * difficulty; self.maxHealth = 70 * difficulty; self.speed = 1.8; self.value = 15; } else if (type === 'slapstickHater') { enemySprite.tint = 0xFF5533; self.health = 60 * difficulty; self.maxHealth = 60 * difficulty; self.speed = 2.2; self.value = 12; } else { // Normal type self.health = 50 * difficulty; self.maxHealth = 50 * difficulty; } // Update health bar self.updateHealthBar(); }; self.takeDamage = function (amount, damageType) { // Apply damage modifiers based on enemy type and damage type var actualDamage = amount; if (self.enemyType === 'punHater' && damageType === 'pun') { actualDamage *= 0.5; // Pun haters take 50% damage from puns } else if (self.enemyType === 'slapstickHater' && damageType === 'slapstick') { actualDamage *= 0.5; // Slapstick haters take 50% damage from slapstick } else if (self.enemyType === 'punHater' && damageType === 'slapstick') { actualDamage *= 1.5; // Pun haters take extra damage from slapstick } else if (self.enemyType === 'slapstickHater' && damageType === 'pun') { actualDamage *= 1.5; // Slapstick haters take extra damage from puns } self.health -= actualDamage; // Update health bar self.updateHealthBar(); // Flash the enemy when taking damage LK.effects.flashObject(enemySprite, 0xFFFFFF, 200); return self.health <= 0; }; self.updateHealthBar = function () { var healthPercentage = Math.max(0, self.health / self.maxHealth); self.healthBar.width = self.healthBarWidth * healthPercentage; // Change color based on health if (healthPercentage > 0.6) { self.healthBar.tint = 0x00FF00; } else if (healthPercentage > 0.3) { self.healthBar.tint = 0xFFFF00; } else { self.healthBar.tint = 0xFF0000; } }; self.update = function () { if (self.reachedEnd || !gamePath || !gamePath.waypoints) { return; } // Get current and next waypoint var currentWaypoint = gamePath.waypoints[self.currentWaypointIndex]; var nextWaypointData = gamePath.getNextWaypoint(self.currentWaypointIndex, { x: self.x, y: self.y }); // If reached end of path if (!nextWaypointData) { self.reachedEnd = true; playerHealth -= 1; healthText.setText("Health: " + playerHealth); if (playerHealth <= 0) { LK.effects.flashScreen(0xFF0000, 1000); LK.showGameOver(); } return; } // Move towards next waypoint var nextWaypoint = nextWaypointData.position; var dx = nextWaypoint.x - self.x; var dy = nextWaypoint.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Reached waypoint, move to next self.currentWaypointIndex++; } else { // Calculate new position var newX = self.x + dx / distance * self.speed; var newY = self.y + dy / distance * self.speed; // Check for collisions with KnockKnock towers var canMove = true; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; // Only check KnockKnock towers if (tower.towerType === 'knockknock') { var tdx = tower.x - newX; var tdy = tower.y - newY; var towerDistance = Math.sqrt(tdx * tdx + tdy * tdy); // If too close to a KnockKnock tower, can't move if (towerDistance < 50) { canMove = false; break; } } } // Only move if not blocked by KnockKnock towers if (canMove) { self.x = newX; self.y = newY; } else { // Try to find an alternative path by slightly changing direction // This creates a more natural "flowing around obstacles" behavior var angles = [Math.PI / 4, -Math.PI / 4, Math.PI / 2, -Math.PI / 2]; for (var i = 0; i < angles.length; i++) { var angle = Math.atan2(dy, dx) + angles[i]; var altX = self.x + Math.cos(angle) * self.speed * 0.5; var altY = self.y + Math.sin(angle) * self.speed * 0.5; // Check if this alternative path is clear var altPathClear = true; for (var j = 0; j < towers.length; j++) { var tower = towers[j]; if (tower.towerType === 'knockknock') { var altDx = tower.x - altX; var altDy = tower.y - altY; var altDist = Math.sqrt(altDx * altDx + altDy * altDy); if (altDist < 50) { altPathClear = false; break; } } } // If found clear path, take it if (altPathClear) { self.x = altX; self.y = altY; break; } } } } }; return self; }); var Path = Container.expand(function () { var self = Container.call(this); self.waypoints = []; self.addWaypoint = function (x, y) { self.waypoints.push({ x: x, y: y }); }; self.getNextWaypoint = function (currentWaypoint, currentPosition) { if (currentWaypoint < self.waypoints.length - 1) { return { index: currentWaypoint + 1, position: self.waypoints[currentWaypoint + 1] }; } return null; }; self.render = function () { if (self.waypoints.length < 2) { return; } for (var i = 0; i < self.waypoints.length - 1; i++) { var start = self.waypoints[i]; var end = self.waypoints[i + 1]; var distance = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); var angle = Math.atan2(end.y - start.y, end.x - start.x); var pathSegment = self.attachAsset('path', { anchorX: 0, anchorY: 0.5, width: distance, height: 80 }); pathSegment.x = start.x; pathSegment.y = start.y; pathSegment.rotation = angle; pathSegment.alpha = 0.5; } }; return self; }); var Projectile = Container.expand(function () { var self = Container.call(this); // Properties self.speed = 10; self.damage = 20; self.target = null; self.damageType = 'normal'; // Create projectile sprite var projectileSprite = self.attachAsset('projectile', { anchorX: 0.5, anchorY: 0.5 }); // Adapt color based on joke type if (self.damageType === 'pun') { projectileSprite.tint = 0x44AADD; } else if (self.damageType === 'slapstick') { projectileSprite.tint = 0xFF6600; } self.update = function () { if (!self.target || !self.target.parent || self.target.health <= 0) { self.destroy(); return; } // Move towards 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 <= self.speed) { // Hit target var killed = self.target.takeDamage(self.damage, self.damageType); if (killed) { // Grant laughs for kill laughs += self.target.value; laughsText.setText("Laughs: " + laughs); // Play death sound LK.getSound('enemyDie').play(); // Remove from enemies array var index = enemies.indexOf(self.target); if (index > -1) { enemies.splice(index, 1); } // Destroy the enemy self.target.destroy(); } // Destroy projectile after hit self.destroy(); } else { // Move towards target self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; // Rotate to face movement direction self.rotation = Math.atan2(dy, dx); } }; return self; }); var Tower = Container.expand(function () { var self = Container.call(this); // Properties self.range = 200; self.damage = 20; self.attackSpeed = 1; // Attacks per second self.attackCooldown = 0; self.level = 1; self.cost = 50; self.upgradeMultiplier = 1.5; self.target = null; self.towerType = 'normal'; // normal, pun, slapstick self.rangeIndicator = null; self.showingRange = false; // Create tower sprite var towerSprite = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); // Methods self.setup = function (type) { self.towerType = type; // Adjust properties based on type if (type === 'pun') { self.removeChildren(); towerSprite = self.attachAsset('punTower', { anchorX: 0.5, anchorY: 0.5 }); self.damage = 15; self.attackSpeed = 1.2; self.range = 250; self.cost = 60; } else if (type === 'slapstick') { self.removeChildren(); towerSprite = self.attachAsset('slapstickTower', { anchorX: 0.5, anchorY: 0.5 }); self.damage = 25; self.attackSpeed = 0.8; self.range = 180; self.cost = 70; } // Create level indicator text var levelText = new Text2("1", { size: 30, fill: 0xFFFFFF }); levelText.anchor.set(0.5, 0.5); levelText.y = -60; self.addChild(levelText); self.levelText = levelText; }; self.toggleRangeIndicator = function () { if (self.showingRange) { if (self.rangeIndicator) { self.removeChild(self.rangeIndicator); self.rangeIndicator = null; } self.showingRange = false; } else { self.rangeIndicator = LK.getAsset('rangeIndicator', { anchorX: 0.5, anchorY: 0.5, width: self.range * 2, height: self.range * 2 }); self.rangeIndicator.alpha = 0.2; self.addChild(self.rangeIndicator); self.showingRange = true; } }; self.upgrade = function () { if (laughs >= self.getUpgradeCost()) { laughs -= self.getUpgradeCost(); laughsText.setText("Laughs: " + laughs); self.level++; self.damage = Math.floor(self.damage * 1.5); self.range *= 1.2; self.attackSpeed *= 1.1; // Update level indicator self.levelText.setText(self.level); // Update range indicator if visible if (self.showingRange) { self.toggleRangeIndicator(); self.toggleRangeIndicator(); } return true; } return false; }; self.getUpgradeCost = function () { return Math.floor(self.cost * Math.pow(self.upgradeMultiplier, self.level)); }; self.findTarget = function (enemies) { // Clear current target if it's not valid if (self.target && (!self.target.parent || self.target.health <= 0 || self.distanceTo(self.target) > self.range)) { self.target = null; } // Find new target if needed if (!self.target) { var closestEnemy = null; var closestDistance = self.range; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > 0) { var distance = self.distanceTo(enemy); if (distance <= self.range && (!closestEnemy || distance < closestDistance)) { closestEnemy = enemy; closestDistance = distance; } } } self.target = closestEnemy; } return self.target; }; self.distanceTo = function (object) { var dx = self.x - object.x; var dy = self.y - object.y; return Math.sqrt(dx * dx + dy * dy); }; self.attack = function () { if (!self.target) { return; } // Create projectile var projectile = new Projectile(); projectile.x = self.x; projectile.y = self.y; projectile.target = self.target; projectile.damage = self.damage; projectile.damageType = self.towerType; game.addChild(projectile); projectiles.push(projectile); // Play attack sound LK.getSound('joke').play(); // Visual feedback tween(towerSprite, { scaleX: 1.2, scaleY: 1.2 }, { duration: 100, onFinish: function onFinish() { tween(towerSprite, { scaleX: 1, scaleY: 1 }, { duration: 100 }); } }); }; self.down = function (x, y, obj) { // Show range and upgrade info self.toggleRangeIndicator(); // Set as selected tower if (selectedTower && selectedTower !== self) { selectedTower.toggleRangeIndicator(); } selectedTower = self; // Update upgrade button var upgradeCost = self.getUpgradeCost(); upgradeButton.setText("Upgrade: " + upgradeCost); upgradeButton.visible = true; // Can upgrade if enough laughs upgradeButton.tint = laughs >= upgradeCost ? 0xFFFFFF : 0xFF0000; }; self.update = function () { // Handle attack cooldown if (self.attackCooldown > 0) { self.attackCooldown -= 1 / 60; // 60 fps } // Only find and attack if cooldown is ready if (self.attackCooldown <= 0) { if (self.findTarget(enemies)) { self.attack(); self.attackCooldown = 1 / self.attackSpeed; } } }; return self; }); var KnockKnockTower = Tower.expand(function () { var self = Tower.call(this); // Override setup method to customize KnockKnock tower var parentSetup = self.setup; self.setup = function (type) { self.towerType = 'knockknock'; self.removeChildren(); towerSprite = self.attachAsset('KnockKnockTower', { anchorX: 0.5, anchorY: 0.5 }); // Special properties for KnockKnock tower self.damage = 5; self.attackSpeed = 0.5; self.range = 150; self.cost = 10; self.spreadRadius = 40; // Reduced from 60 to make a tighter wall self.duplicateCount = 0; // Track how many times this tower has duplicated self.maxDuplicates = 30; // Increased from 20 to create a bigger wall self.duplicateInterval = 1; // Reduced from 2 to build wall faster self.duplicateTimer = 0; // Create level indicator text var levelText = new Text2("1", { size: 30, fill: 0xFFFFFF }); levelText.anchor.set(0.5, 0.5); levelText.y = -60; self.addChild(levelText); self.levelText = levelText; }; // Override attack method to do nothing self.attack = function () { // Intentionally empty - KnockKnock towers don't attack }; // Override update to add duplication logic - don't shoot, just duplicate self.update = function () { // Do NOT call parent update (attack logic) - this tower doesn't shoot // Only handle duplication if (self.duplicateCount < self.maxDuplicates) { self.duplicateTimer += 1 / 60; // Assuming 60 FPS if (self.duplicateTimer >= self.duplicateInterval) { self.duplicateTimer = 0; self.duplicate(); } } // Handle attack cooldown (needed to maintain parent behavior) if (self.attackCooldown > 0) { self.attackCooldown -= 1 / 60; // 60 fps } // But don't call attack method }; // Create duplicate towers in line to form a wall self.duplicate = function () { if (self.duplicateCount >= self.maxDuplicates) { return; } // Try to duplicate in all four directions to create a solid wall var directions = [{ dx: self.spreadRadius, dy: 0 }, // right { dx: -self.spreadRadius, dy: 0 }, // left { dx: 0, dy: self.spreadRadius }, // down { dx: 0, dy: -self.spreadRadius } // up ]; for (var i = 0; i < directions.length; i++) { var direction = directions[i]; var newX = self.x + direction.dx; var newY = self.y + direction.dy; // Check if position is valid (not on the path and not occupied) if (self.isValidPosition(newX, newY)) { // Create new tower var newTower = new KnockKnockTower(); newTower.setup('knockknock'); newTower.x = newX; newTower.y = newY; // Set duplicate count high so it doesn't duplicate too much newTower.duplicateCount = self.duplicateCount + 1; game.addChild(newTower); towers.push(newTower); self.duplicateCount++; // Play build sound LK.getSound('buildTower').play(); break; // Only place one tower per duplication } } }; // Check if position is valid for tower placement self.isValidPosition = function (x, y) { // KnockKnock towers can be placed on paths, so we don't check for path collision // Check if tower already exists at position for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - x; var dy = tower.y - y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 50) { // Minimum distance between towers return false; } } // Check for enemies - don't place tower on top of enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - x; var dy = enemy.y - y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 70) { // Don't block too close to enemies that are already on the path return false; } } return true; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x4B6F44 }); /**** * Game Code ****/ // Game variables var gamePath; var waves = []; var currentWave = 0; var enemies = []; var towers = []; var projectiles = []; var buildSpots = []; var laughs = 100; // Starting currency var playerHealth = 10; var waveInProgress = false; var nextWaveCountdown = 10; // Seconds until next wave var selectedTower = null; var buildMenu; var buildMenuSpot = null; var spawnInterval; // UI Elements var laughsText; var healthText; var waveText = new Text2("Wave: 0/0", { size: 40, fill: 0xFFFFFF }); var nextWaveText; var startWaveButton; var upgradeButton; var normalTowerButton; var punTowerButton; var slapstickTowerButton; var knockKnockTowerButton; // Initialize UI function initUI() { // Create score text laughsText = new Text2("Laughs: " + laughs, { size: 40, fill: 0xFFFFFF }); laughsText.anchor.set(0, 0); laughsText.x = 120; laughsText.y = 20; LK.gui.addChild(laughsText); // Create health text healthText = new Text2("Health: " + playerHealth, { size: 40, fill: 0xFFFFFF }); healthText.anchor.set(0, 0); healthText.x = 120; healthText.y = 70; LK.gui.addChild(healthText); // Create wave text waveText = new Text2("Wave: 0/" + waves.length, { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(0, 0); waveText.x = 120; waveText.y = 120; LK.gui.addChild(waveText); // Create next wave text nextWaveText = new Text2("Next wave: " + nextWaveCountdown, { size: 40, fill: 0xFFFFFF }); nextWaveText.anchor.set(0.5, 0); nextWaveText.x = 1024; nextWaveText.y = 20; LK.gui.addChild(nextWaveText); // Create start wave button var buttonAsset = LK.getAsset('StartWaveButton', { anchorX: 0.5, anchorY: 0.5 }); startWaveButton = new Container(); startWaveButton.addChild(buttonAsset); // Add text to button var buttonText = new Text2("Start Wave", { size: 40, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); startWaveButton.addChild(buttonText); // Position button on the map (not in the GUI) startWaveButton.x = 1900; startWaveButton.y = 300; startWaveButton.interactive = true; game.addChild(startWaveButton); startWaveButton.down = function () { if (!waveInProgress && currentWave < waves.length) { startWave(); nextWaveCountdown = 0; // Reset countdown to avoid timer interference } }; // Create upgrade button (hidden initially) upgradeButton = new Text2("Upgrade: 50", { size: 40, fill: 0xFFFFFF }); upgradeButton.anchor.set(0.5, 0); upgradeButton.x = 1024; upgradeButton.y = 2650; upgradeButton.visible = false; upgradeButton.interactive = true; LK.gui.addChild(upgradeButton); upgradeButton.down = function () { if (selectedTower) { if (selectedTower.upgrade()) { var upgradeCost = selectedTower.getUpgradeCost(); upgradeButton.setText("Upgrade: " + upgradeCost); upgradeButton.tint = laughs >= upgradeCost ? 0xFFFFFF : 0xFF0000; } } }; // Create build menu (hidden initially) buildMenu = new Container(); buildMenu.visible = false; game.addChild(buildMenu); // Normal tower button normalTowerButton = new Text2("Normal Tower (10)", { size: 30, fill: 0xFFFFFF }); normalTowerButton.anchor.set(0.5, 0.5); normalTowerButton.y = -120; normalTowerButton.interactive = true; buildMenu.addChild(normalTowerButton); normalTowerButton.down = function () { if (buildMenuSpot) { buildMenuSpot.buildTower('normal'); } }; // Pun tower button punTowerButton = new Text2("Pun Tower (10)", { size: 30, fill: 0xFFFFFF }); punTowerButton.anchor.set(0.5, 0.5); punTowerButton.y = -60; punTowerButton.interactive = true; buildMenu.addChild(punTowerButton); punTowerButton.down = function () { if (buildMenuSpot) { buildMenuSpot.buildTower('pun'); } }; // Slapstick tower button slapstickTowerButton = new Text2("Slapstick Tower (10)", { size: 30, fill: 0xFFFFFF }); slapstickTowerButton.anchor.set(0.5, 0.5); slapstickTowerButton.y = 0; slapstickTowerButton.interactive = true; buildMenu.addChild(slapstickTowerButton); slapstickTowerButton.down = function () { if (buildMenuSpot) { buildMenuSpot.buildTower('slapstick'); } }; // KnockKnock tower button knockKnockTowerButton = new Text2("KnockKnock Tower (10)", { size: 30, fill: 0xFFFFFF }); knockKnockTowerButton.anchor.set(0.5, 0.5); knockKnockTowerButton.y = 60; knockKnockTowerButton.interactive = true; buildMenu.addChild(knockKnockTowerButton); knockKnockTowerButton.down = function () { if (buildMenuSpot) { buildMenuSpot.buildTower('knockknock'); } }; } function showBuildMenu(spot) { if (buildMenu.visible && buildMenuSpot === spot) { hideBuildMenu(); return; } buildMenuSpot = spot; buildMenu.x = spot.x; buildMenu.y = spot.y; buildMenu.visible = true; // Update button colors based on affordability normalTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; punTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; slapstickTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; knockKnockTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; } function hideBuildMenu() { buildMenu.visible = false; buildMenuSpot = null; } // Create game path function createPath() { gamePath = new Path(); // Add waypoints (customize these for your map) gamePath.addWaypoint(0, 700); gamePath.addWaypoint(400, 700); gamePath.addWaypoint(400, 1100); gamePath.addWaypoint(1100, 1100); gamePath.addWaypoint(1100, 1500); gamePath.addWaypoint(400, 1500); gamePath.addWaypoint(400, 1900); gamePath.addWaypoint(1600, 1900); gamePath.addWaypoint(1600, 700); gamePath.addWaypoint(2048, 700); // Render path gamePath.render(); game.addChild(gamePath); // Create Punchline (the end point to defend) var punchline = LK.getAsset('punchline', { anchorX: 0.5, anchorY: 0.5 }); punchline.x = 2048 - 100; punchline.y = 700; game.addChild(punchline); // Create build spots along the path createBuildSpots(); } function createBuildSpots() { // Create build spots strategically around the path var spots = [{ x: 300, y: 550 }, { x: 300, y: 850 }, { x: 550, y: 1000 }, { x: 550, y: 1200 }, { x: 950, y: 1000 }, { x: 950, y: 1200 }, { x: 950, y: 1350 }, { x: 950, y: 1650 }, { x: 550, y: 1400 }, { x: 550, y: 1600 }, { x: 300, y: 1750 }, { x: 300, y: 2050 }, { x: 550, y: 1900 }, { x: 800, y: 1750 }, { x: 800, y: 2050 }, { x: 1400, y: 1750 }, { x: 1400, y: 2050 }, { x: 1800, y: 1900 }, { x: 1450, y: 850 }, { x: 1450, y: 550 }, { x: 1800, y: 700 }]; for (var i = 0; i < spots.length; i++) { var spot = new BuildSpot(); spot.x = spots[i].x; spot.y = spots[i].y; buildSpots.push(spot); game.addChild(spot); } } // Define waves function createWaves() { // Wave 1: 10 normal enemies waves.push({ enemies: [{ type: 'normal', count: 10, delay: 1.5, difficulty: 1 }] }); // Wave 2: Mix of normal and pun haters waves.push({ enemies: [{ type: 'normal', count: 8, delay: 1.2, difficulty: 1 }, { type: 'punHater', count: 5, delay: 2, difficulty: 1 }] }); // Wave 3: Mix of all types waves.push({ enemies: [{ type: 'normal', count: 10, delay: 1, difficulty: 1.2 }, { type: 'punHater', count: 7, delay: 1.5, difficulty: 1.2 }, { type: 'slapstickHater', count: 7, delay: 1.5, difficulty: 1.2 }] }); // Wave 4: More enemies, tougher waves.push({ enemies: [{ type: 'normal', count: 15, delay: 0.8, difficulty: 1.5 }, { type: 'punHater', count: 10, delay: 1.2, difficulty: 1.5 }, { type: 'slapstickHater', count: 10, delay: 1.2, difficulty: 1.5 }] }); // Wave 5: Final wave, very tough waves.push({ enemies: [{ type: 'normal', count: 20, delay: 0.7, difficulty: 2 }, { type: 'punHater', count: 15, delay: 1, difficulty: 2 }, { type: 'slapstickHater', count: 15, delay: 1, difficulty: 2 }] }); // Only update waveText if it has been initialized if (waveText) { waveText.setText("Wave: " + currentWave + "/" + waves.length); } } // Start a wave function startWave() { if (waveInProgress || currentWave >= waves.length) { return; } waveInProgress = true; currentWave++; waveText.setText("Wave: " + currentWave + "/" + waves.length); nextWaveText.visible = false; startWaveButton.visible = false; var wave = waves[currentWave - 1]; var enemyGroups = wave.enemies; var enemyTypesProcessed = 0; function spawnEnemyGroup(groupIndex) { if (groupIndex >= enemyGroups.length) { enemyTypesProcessed++; if (enemyTypesProcessed >= enemyGroups.length) { // All enemy types processed, clear interval LK.clearInterval(spawnInterval); // Check for wave completion periodically var checkInterval = LK.setInterval(function () { if (enemies.length === 0) { waveInProgress = false; LK.clearInterval(checkInterval); // Last wave completed if (currentWave >= waves.length) { // Player wins LK.showYouWin(); } else { // Prepare for next wave nextWaveCountdown = 15; nextWaveText.setText("Next wave: " + nextWaveCountdown); nextWaveText.visible = true; startWaveButton.visible = true; // Bonus laughs for completing the wave laughs += 50 + currentWave * 10; laughsText.setText("Laughs: " + laughs); } } }, 1000); return; } // Move to next enemy type groupIndex = 0; } var group = enemyGroups[groupIndex]; if (group.count > 0) { // Spawn enemy var enemy = new Enemy(); enemy.setup(group.type, group.difficulty); enemy.x = gamePath.waypoints[0].x; enemy.y = gamePath.waypoints[0].y; game.addChild(enemy); enemies.push(enemy); // Decrease count group.count--; // Schedule next spawn for this group LK.setTimeout(function () { spawnEnemyGroup(groupIndex); }, group.delay * 1000); } else { // Move to next group spawnEnemyGroup(groupIndex + 1); } } // Start spawning enemies spawnInterval = LK.setInterval(function () { spawnEnemyGroup(0); }, 100); } // Initialize game function initGame() { // Create path createPath(); // Create waves createWaves(); // Init UI initUI(); // Start countdown timer for first wave var countdownInterval = LK.setInterval(function () { if (!waveInProgress) { nextWaveCountdown--; nextWaveText.setText("Next wave: " + nextWaveCountdown); if (nextWaveCountdown <= 0) { LK.clearInterval(countdownInterval); startWave(); } } }, 1000); // Play background music LK.playMusic('bgMusic', { fade: { start: 0, end: 0.3, duration: 1000 } }); } // Initialize game initGame(); // Global event handlers game.down = function (x, y, obj) { // Hide build menu if clicking outside if (buildMenu.visible) { var clickedInMenu = false; // Check if clicked on the menu if (x >= buildMenu.x - 150 && x <= buildMenu.x + 150 && y >= buildMenu.y - 150 && y <= buildMenu.y + 150) { clickedInMenu = true; } if (!clickedInMenu) { hideBuildMenu(); } } // Hide tower info if clicking outside if (selectedTower) { var clickedOnTower = false; for (var i = 0; i < towers.length; i++) { if (towers[i] === obj) { clickedOnTower = true; break; } } if (!clickedOnTower) { if (selectedTower.showingRange) { selectedTower.toggleRangeIndicator(); } selectedTower = null; upgradeButton.visible = false; } } }; // Main game update game.update = function () { // Update all enemies for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; // If enemy is dead but not yet removed if (enemy.health <= 0) { enemies.splice(i, 1); enemy.destroy(); continue; } // If enemy reached the end if (enemy.reachedEnd) { enemies.splice(i, 1); enemy.destroy(); continue; } } // Update all projectiles for (var j = projectiles.length - 1; j >= 0; j--) { var projectile = projectiles[j]; // If projectile is missing if (!projectile.parent) { projectiles.splice(j, 1); } } // Update UI button states if (upgradeButton.visible && selectedTower) { var upgradeCost = selectedTower.getUpgradeCost(); upgradeButton.tint = laughs >= upgradeCost ? 0xFFFFFF : 0xFF0000; } if (buildMenu.visible) { normalTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; punTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; slapstickTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; knockKnockTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000; } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var BuildSpot = Container.expand(function () {
var self = Container.call(this);
// Override findTarget method so KnockKnock tower can detect enemies
self.findTarget = function (enemies) {
// Find closest enemy
var closestEnemy = null;
var closestDistance = self.range;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > 0) {
var distance = self.distanceTo(enemy);
if (distance <= self.range && (!closestEnemy || distance < closestDistance)) {
closestEnemy = enemy;
closestDistance = distance;
}
}
}
self.target = closestEnemy;
return self.target;
};
self.hasTower = false;
self.towerType = null;
// Create build spot sprite
var spotSprite = self.attachAsset('buildSpot', {
anchorX: 0.5,
anchorY: 0.5
});
spotSprite.alpha = 0.5;
self.down = function (x, y, obj) {
if (self.hasTower) {
return;
}
// Toggle build menu visibility
if (buildMenu.visible && buildMenuSpot === self) {
hideBuildMenu();
} else {
showBuildMenu(self);
}
};
self.buildTower = function (type) {
if (self.hasTower) {
return false;
}
var towerCost = 10;
if (type === 'pun') {
towerCost = 10;
}
if (type === 'slapstick') {
towerCost = 10;
}
if (type === 'knockknock') {
towerCost = 10;
}
if (laughs >= towerCost) {
// Deduct cost
laughs -= towerCost;
laughsText.setText("Laughs: " + laughs);
// Create tower
var tower;
if (type === 'knockknock') {
tower = new KnockKnockTower();
} else {
tower = new Tower();
}
tower.setup(type);
tower.x = self.x;
tower.y = self.y;
game.addChild(tower);
towers.push(tower);
// Mark spot as used
self.hasTower = true;
self.towerType = type;
spotSprite.alpha = 0.2;
// Play build sound
LK.getSound('buildTower').play();
// Hide build menu
hideBuildMenu();
return true;
}
return false;
};
return self;
});
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Properties
self.health = 100;
self.maxHealth = 100;
self.speed = 2;
self.value = 10; // Laughs earned when killed
self.currentWaypointIndex = 0;
self.reachedEnd = false;
self.enemyType = 'normal'; // normal, punHater, slapstickHater
self.healthBarWidth = 80;
// Create enemy sprite
var enemySprite = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Create health bar
var healthBarBackground = LK.getAsset('path', {
width: self.healthBarWidth,
height: 10,
color: 0x333333,
anchorX: 0.5,
anchorY: 0.5
});
healthBarBackground.y = -50;
self.addChild(healthBarBackground);
var healthBar = LK.getAsset('path', {
width: self.healthBarWidth,
height: 10,
color: 0x00FF00,
anchorX: 0,
anchorY: 0.5
});
healthBar.x = -self.healthBarWidth / 2;
healthBar.y = -50;
self.addChild(healthBar);
self.healthBar = healthBar;
// Methods
self.setup = function (type, difficulty) {
self.enemyType = type;
// Adjust properties based on type
if (type === 'punHater') {
enemySprite.tint = 0x3355FF;
self.health = 70 * difficulty;
self.maxHealth = 70 * difficulty;
self.speed = 1.8;
self.value = 15;
} else if (type === 'slapstickHater') {
enemySprite.tint = 0xFF5533;
self.health = 60 * difficulty;
self.maxHealth = 60 * difficulty;
self.speed = 2.2;
self.value = 12;
} else {
// Normal type
self.health = 50 * difficulty;
self.maxHealth = 50 * difficulty;
}
// Update health bar
self.updateHealthBar();
};
self.takeDamage = function (amount, damageType) {
// Apply damage modifiers based on enemy type and damage type
var actualDamage = amount;
if (self.enemyType === 'punHater' && damageType === 'pun') {
actualDamage *= 0.5; // Pun haters take 50% damage from puns
} else if (self.enemyType === 'slapstickHater' && damageType === 'slapstick') {
actualDamage *= 0.5; // Slapstick haters take 50% damage from slapstick
} else if (self.enemyType === 'punHater' && damageType === 'slapstick') {
actualDamage *= 1.5; // Pun haters take extra damage from slapstick
} else if (self.enemyType === 'slapstickHater' && damageType === 'pun') {
actualDamage *= 1.5; // Slapstick haters take extra damage from puns
}
self.health -= actualDamage;
// Update health bar
self.updateHealthBar();
// Flash the enemy when taking damage
LK.effects.flashObject(enemySprite, 0xFFFFFF, 200);
return self.health <= 0;
};
self.updateHealthBar = function () {
var healthPercentage = Math.max(0, self.health / self.maxHealth);
self.healthBar.width = self.healthBarWidth * healthPercentage;
// Change color based on health
if (healthPercentage > 0.6) {
self.healthBar.tint = 0x00FF00;
} else if (healthPercentage > 0.3) {
self.healthBar.tint = 0xFFFF00;
} else {
self.healthBar.tint = 0xFF0000;
}
};
self.update = function () {
if (self.reachedEnd || !gamePath || !gamePath.waypoints) {
return;
}
// Get current and next waypoint
var currentWaypoint = gamePath.waypoints[self.currentWaypointIndex];
var nextWaypointData = gamePath.getNextWaypoint(self.currentWaypointIndex, {
x: self.x,
y: self.y
});
// If reached end of path
if (!nextWaypointData) {
self.reachedEnd = true;
playerHealth -= 1;
healthText.setText("Health: " + playerHealth);
if (playerHealth <= 0) {
LK.effects.flashScreen(0xFF0000, 1000);
LK.showGameOver();
}
return;
}
// Move towards next waypoint
var nextWaypoint = nextWaypointData.position;
var dx = nextWaypoint.x - self.x;
var dy = nextWaypoint.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Reached waypoint, move to next
self.currentWaypointIndex++;
} else {
// Calculate new position
var newX = self.x + dx / distance * self.speed;
var newY = self.y + dy / distance * self.speed;
// Check for collisions with KnockKnock towers
var canMove = true;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Only check KnockKnock towers
if (tower.towerType === 'knockknock') {
var tdx = tower.x - newX;
var tdy = tower.y - newY;
var towerDistance = Math.sqrt(tdx * tdx + tdy * tdy);
// If too close to a KnockKnock tower, can't move
if (towerDistance < 50) {
canMove = false;
break;
}
}
}
// Only move if not blocked by KnockKnock towers
if (canMove) {
self.x = newX;
self.y = newY;
} else {
// Try to find an alternative path by slightly changing direction
// This creates a more natural "flowing around obstacles" behavior
var angles = [Math.PI / 4, -Math.PI / 4, Math.PI / 2, -Math.PI / 2];
for (var i = 0; i < angles.length; i++) {
var angle = Math.atan2(dy, dx) + angles[i];
var altX = self.x + Math.cos(angle) * self.speed * 0.5;
var altY = self.y + Math.sin(angle) * self.speed * 0.5;
// Check if this alternative path is clear
var altPathClear = true;
for (var j = 0; j < towers.length; j++) {
var tower = towers[j];
if (tower.towerType === 'knockknock') {
var altDx = tower.x - altX;
var altDy = tower.y - altY;
var altDist = Math.sqrt(altDx * altDx + altDy * altDy);
if (altDist < 50) {
altPathClear = false;
break;
}
}
}
// If found clear path, take it
if (altPathClear) {
self.x = altX;
self.y = altY;
break;
}
}
}
}
};
return self;
});
var Path = Container.expand(function () {
var self = Container.call(this);
self.waypoints = [];
self.addWaypoint = function (x, y) {
self.waypoints.push({
x: x,
y: y
});
};
self.getNextWaypoint = function (currentWaypoint, currentPosition) {
if (currentWaypoint < self.waypoints.length - 1) {
return {
index: currentWaypoint + 1,
position: self.waypoints[currentWaypoint + 1]
};
}
return null;
};
self.render = function () {
if (self.waypoints.length < 2) {
return;
}
for (var i = 0; i < self.waypoints.length - 1; i++) {
var start = self.waypoints[i];
var end = self.waypoints[i + 1];
var distance = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));
var angle = Math.atan2(end.y - start.y, end.x - start.x);
var pathSegment = self.attachAsset('path', {
anchorX: 0,
anchorY: 0.5,
width: distance,
height: 80
});
pathSegment.x = start.x;
pathSegment.y = start.y;
pathSegment.rotation = angle;
pathSegment.alpha = 0.5;
}
};
return self;
});
var Projectile = Container.expand(function () {
var self = Container.call(this);
// Properties
self.speed = 10;
self.damage = 20;
self.target = null;
self.damageType = 'normal';
// Create projectile sprite
var projectileSprite = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5
});
// Adapt color based on joke type
if (self.damageType === 'pun') {
projectileSprite.tint = 0x44AADD;
} else if (self.damageType === 'slapstick') {
projectileSprite.tint = 0xFF6600;
}
self.update = function () {
if (!self.target || !self.target.parent || self.target.health <= 0) {
self.destroy();
return;
}
// Move towards 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 <= self.speed) {
// Hit target
var killed = self.target.takeDamage(self.damage, self.damageType);
if (killed) {
// Grant laughs for kill
laughs += self.target.value;
laughsText.setText("Laughs: " + laughs);
// Play death sound
LK.getSound('enemyDie').play();
// Remove from enemies array
var index = enemies.indexOf(self.target);
if (index > -1) {
enemies.splice(index, 1);
}
// Destroy the enemy
self.target.destroy();
}
// Destroy projectile after hit
self.destroy();
} else {
// Move towards target
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
// Rotate to face movement direction
self.rotation = Math.atan2(dy, dx);
}
};
return self;
});
var Tower = Container.expand(function () {
var self = Container.call(this);
// Properties
self.range = 200;
self.damage = 20;
self.attackSpeed = 1; // Attacks per second
self.attackCooldown = 0;
self.level = 1;
self.cost = 50;
self.upgradeMultiplier = 1.5;
self.target = null;
self.towerType = 'normal'; // normal, pun, slapstick
self.rangeIndicator = null;
self.showingRange = false;
// Create tower sprite
var towerSprite = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
// Methods
self.setup = function (type) {
self.towerType = type;
// Adjust properties based on type
if (type === 'pun') {
self.removeChildren();
towerSprite = self.attachAsset('punTower', {
anchorX: 0.5,
anchorY: 0.5
});
self.damage = 15;
self.attackSpeed = 1.2;
self.range = 250;
self.cost = 60;
} else if (type === 'slapstick') {
self.removeChildren();
towerSprite = self.attachAsset('slapstickTower', {
anchorX: 0.5,
anchorY: 0.5
});
self.damage = 25;
self.attackSpeed = 0.8;
self.range = 180;
self.cost = 70;
}
// Create level indicator text
var levelText = new Text2("1", {
size: 30,
fill: 0xFFFFFF
});
levelText.anchor.set(0.5, 0.5);
levelText.y = -60;
self.addChild(levelText);
self.levelText = levelText;
};
self.toggleRangeIndicator = function () {
if (self.showingRange) {
if (self.rangeIndicator) {
self.removeChild(self.rangeIndicator);
self.rangeIndicator = null;
}
self.showingRange = false;
} else {
self.rangeIndicator = LK.getAsset('rangeIndicator', {
anchorX: 0.5,
anchorY: 0.5,
width: self.range * 2,
height: self.range * 2
});
self.rangeIndicator.alpha = 0.2;
self.addChild(self.rangeIndicator);
self.showingRange = true;
}
};
self.upgrade = function () {
if (laughs >= self.getUpgradeCost()) {
laughs -= self.getUpgradeCost();
laughsText.setText("Laughs: " + laughs);
self.level++;
self.damage = Math.floor(self.damage * 1.5);
self.range *= 1.2;
self.attackSpeed *= 1.1;
// Update level indicator
self.levelText.setText(self.level);
// Update range indicator if visible
if (self.showingRange) {
self.toggleRangeIndicator();
self.toggleRangeIndicator();
}
return true;
}
return false;
};
self.getUpgradeCost = function () {
return Math.floor(self.cost * Math.pow(self.upgradeMultiplier, self.level));
};
self.findTarget = function (enemies) {
// Clear current target if it's not valid
if (self.target && (!self.target.parent || self.target.health <= 0 || self.distanceTo(self.target) > self.range)) {
self.target = null;
}
// Find new target if needed
if (!self.target) {
var closestEnemy = null;
var closestDistance = self.range;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > 0) {
var distance = self.distanceTo(enemy);
if (distance <= self.range && (!closestEnemy || distance < closestDistance)) {
closestEnemy = enemy;
closestDistance = distance;
}
}
}
self.target = closestEnemy;
}
return self.target;
};
self.distanceTo = function (object) {
var dx = self.x - object.x;
var dy = self.y - object.y;
return Math.sqrt(dx * dx + dy * dy);
};
self.attack = function () {
if (!self.target) {
return;
}
// Create projectile
var projectile = new Projectile();
projectile.x = self.x;
projectile.y = self.y;
projectile.target = self.target;
projectile.damage = self.damage;
projectile.damageType = self.towerType;
game.addChild(projectile);
projectiles.push(projectile);
// Play attack sound
LK.getSound('joke').play();
// Visual feedback
tween(towerSprite, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
onFinish: function onFinish() {
tween(towerSprite, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
}
});
};
self.down = function (x, y, obj) {
// Show range and upgrade info
self.toggleRangeIndicator();
// Set as selected tower
if (selectedTower && selectedTower !== self) {
selectedTower.toggleRangeIndicator();
}
selectedTower = self;
// Update upgrade button
var upgradeCost = self.getUpgradeCost();
upgradeButton.setText("Upgrade: " + upgradeCost);
upgradeButton.visible = true;
// Can upgrade if enough laughs
upgradeButton.tint = laughs >= upgradeCost ? 0xFFFFFF : 0xFF0000;
};
self.update = function () {
// Handle attack cooldown
if (self.attackCooldown > 0) {
self.attackCooldown -= 1 / 60; // 60 fps
}
// Only find and attack if cooldown is ready
if (self.attackCooldown <= 0) {
if (self.findTarget(enemies)) {
self.attack();
self.attackCooldown = 1 / self.attackSpeed;
}
}
};
return self;
});
var KnockKnockTower = Tower.expand(function () {
var self = Tower.call(this);
// Override setup method to customize KnockKnock tower
var parentSetup = self.setup;
self.setup = function (type) {
self.towerType = 'knockknock';
self.removeChildren();
towerSprite = self.attachAsset('KnockKnockTower', {
anchorX: 0.5,
anchorY: 0.5
});
// Special properties for KnockKnock tower
self.damage = 5;
self.attackSpeed = 0.5;
self.range = 150;
self.cost = 10;
self.spreadRadius = 40; // Reduced from 60 to make a tighter wall
self.duplicateCount = 0; // Track how many times this tower has duplicated
self.maxDuplicates = 30; // Increased from 20 to create a bigger wall
self.duplicateInterval = 1; // Reduced from 2 to build wall faster
self.duplicateTimer = 0;
// Create level indicator text
var levelText = new Text2("1", {
size: 30,
fill: 0xFFFFFF
});
levelText.anchor.set(0.5, 0.5);
levelText.y = -60;
self.addChild(levelText);
self.levelText = levelText;
};
// Override attack method to do nothing
self.attack = function () {
// Intentionally empty - KnockKnock towers don't attack
};
// Override update to add duplication logic - don't shoot, just duplicate
self.update = function () {
// Do NOT call parent update (attack logic) - this tower doesn't shoot
// Only handle duplication
if (self.duplicateCount < self.maxDuplicates) {
self.duplicateTimer += 1 / 60; // Assuming 60 FPS
if (self.duplicateTimer >= self.duplicateInterval) {
self.duplicateTimer = 0;
self.duplicate();
}
}
// Handle attack cooldown (needed to maintain parent behavior)
if (self.attackCooldown > 0) {
self.attackCooldown -= 1 / 60; // 60 fps
}
// But don't call attack method
};
// Create duplicate towers in line to form a wall
self.duplicate = function () {
if (self.duplicateCount >= self.maxDuplicates) {
return;
}
// Try to duplicate in all four directions to create a solid wall
var directions = [{
dx: self.spreadRadius,
dy: 0
},
// right
{
dx: -self.spreadRadius,
dy: 0
},
// left
{
dx: 0,
dy: self.spreadRadius
},
// down
{
dx: 0,
dy: -self.spreadRadius
} // up
];
for (var i = 0; i < directions.length; i++) {
var direction = directions[i];
var newX = self.x + direction.dx;
var newY = self.y + direction.dy;
// Check if position is valid (not on the path and not occupied)
if (self.isValidPosition(newX, newY)) {
// Create new tower
var newTower = new KnockKnockTower();
newTower.setup('knockknock');
newTower.x = newX;
newTower.y = newY;
// Set duplicate count high so it doesn't duplicate too much
newTower.duplicateCount = self.duplicateCount + 1;
game.addChild(newTower);
towers.push(newTower);
self.duplicateCount++;
// Play build sound
LK.getSound('buildTower').play();
break; // Only place one tower per duplication
}
}
};
// Check if position is valid for tower placement
self.isValidPosition = function (x, y) {
// KnockKnock towers can be placed on paths, so we don't check for path collision
// Check if tower already exists at position
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - x;
var dy = tower.y - y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 50) {
// Minimum distance between towers
return false;
}
}
// Check for enemies - don't place tower on top of enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - x;
var dy = enemy.y - y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 70) {
// Don't block too close to enemies that are already on the path
return false;
}
}
return true;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x4B6F44
});
/****
* Game Code
****/
// Game variables
var gamePath;
var waves = [];
var currentWave = 0;
var enemies = [];
var towers = [];
var projectiles = [];
var buildSpots = [];
var laughs = 100; // Starting currency
var playerHealth = 10;
var waveInProgress = false;
var nextWaveCountdown = 10; // Seconds until next wave
var selectedTower = null;
var buildMenu;
var buildMenuSpot = null;
var spawnInterval;
// UI Elements
var laughsText;
var healthText;
var waveText = new Text2("Wave: 0/0", {
size: 40,
fill: 0xFFFFFF
});
var nextWaveText;
var startWaveButton;
var upgradeButton;
var normalTowerButton;
var punTowerButton;
var slapstickTowerButton;
var knockKnockTowerButton;
// Initialize UI
function initUI() {
// Create score text
laughsText = new Text2("Laughs: " + laughs, {
size: 40,
fill: 0xFFFFFF
});
laughsText.anchor.set(0, 0);
laughsText.x = 120;
laughsText.y = 20;
LK.gui.addChild(laughsText);
// Create health text
healthText = new Text2("Health: " + playerHealth, {
size: 40,
fill: 0xFFFFFF
});
healthText.anchor.set(0, 0);
healthText.x = 120;
healthText.y = 70;
LK.gui.addChild(healthText);
// Create wave text
waveText = new Text2("Wave: 0/" + waves.length, {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(0, 0);
waveText.x = 120;
waveText.y = 120;
LK.gui.addChild(waveText);
// Create next wave text
nextWaveText = new Text2("Next wave: " + nextWaveCountdown, {
size: 40,
fill: 0xFFFFFF
});
nextWaveText.anchor.set(0.5, 0);
nextWaveText.x = 1024;
nextWaveText.y = 20;
LK.gui.addChild(nextWaveText);
// Create start wave button
var buttonAsset = LK.getAsset('StartWaveButton', {
anchorX: 0.5,
anchorY: 0.5
});
startWaveButton = new Container();
startWaveButton.addChild(buttonAsset);
// Add text to button
var buttonText = new Text2("Start Wave", {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
startWaveButton.addChild(buttonText);
// Position button on the map (not in the GUI)
startWaveButton.x = 1900;
startWaveButton.y = 300;
startWaveButton.interactive = true;
game.addChild(startWaveButton);
startWaveButton.down = function () {
if (!waveInProgress && currentWave < waves.length) {
startWave();
nextWaveCountdown = 0; // Reset countdown to avoid timer interference
}
};
// Create upgrade button (hidden initially)
upgradeButton = new Text2("Upgrade: 50", {
size: 40,
fill: 0xFFFFFF
});
upgradeButton.anchor.set(0.5, 0);
upgradeButton.x = 1024;
upgradeButton.y = 2650;
upgradeButton.visible = false;
upgradeButton.interactive = true;
LK.gui.addChild(upgradeButton);
upgradeButton.down = function () {
if (selectedTower) {
if (selectedTower.upgrade()) {
var upgradeCost = selectedTower.getUpgradeCost();
upgradeButton.setText("Upgrade: " + upgradeCost);
upgradeButton.tint = laughs >= upgradeCost ? 0xFFFFFF : 0xFF0000;
}
}
};
// Create build menu (hidden initially)
buildMenu = new Container();
buildMenu.visible = false;
game.addChild(buildMenu);
// Normal tower button
normalTowerButton = new Text2("Normal Tower (10)", {
size: 30,
fill: 0xFFFFFF
});
normalTowerButton.anchor.set(0.5, 0.5);
normalTowerButton.y = -120;
normalTowerButton.interactive = true;
buildMenu.addChild(normalTowerButton);
normalTowerButton.down = function () {
if (buildMenuSpot) {
buildMenuSpot.buildTower('normal');
}
};
// Pun tower button
punTowerButton = new Text2("Pun Tower (10)", {
size: 30,
fill: 0xFFFFFF
});
punTowerButton.anchor.set(0.5, 0.5);
punTowerButton.y = -60;
punTowerButton.interactive = true;
buildMenu.addChild(punTowerButton);
punTowerButton.down = function () {
if (buildMenuSpot) {
buildMenuSpot.buildTower('pun');
}
};
// Slapstick tower button
slapstickTowerButton = new Text2("Slapstick Tower (10)", {
size: 30,
fill: 0xFFFFFF
});
slapstickTowerButton.anchor.set(0.5, 0.5);
slapstickTowerButton.y = 0;
slapstickTowerButton.interactive = true;
buildMenu.addChild(slapstickTowerButton);
slapstickTowerButton.down = function () {
if (buildMenuSpot) {
buildMenuSpot.buildTower('slapstick');
}
};
// KnockKnock tower button
knockKnockTowerButton = new Text2("KnockKnock Tower (10)", {
size: 30,
fill: 0xFFFFFF
});
knockKnockTowerButton.anchor.set(0.5, 0.5);
knockKnockTowerButton.y = 60;
knockKnockTowerButton.interactive = true;
buildMenu.addChild(knockKnockTowerButton);
knockKnockTowerButton.down = function () {
if (buildMenuSpot) {
buildMenuSpot.buildTower('knockknock');
}
};
}
function showBuildMenu(spot) {
if (buildMenu.visible && buildMenuSpot === spot) {
hideBuildMenu();
return;
}
buildMenuSpot = spot;
buildMenu.x = spot.x;
buildMenu.y = spot.y;
buildMenu.visible = true;
// Update button colors based on affordability
normalTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
punTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
slapstickTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
knockKnockTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
}
function hideBuildMenu() {
buildMenu.visible = false;
buildMenuSpot = null;
}
// Create game path
function createPath() {
gamePath = new Path();
// Add waypoints (customize these for your map)
gamePath.addWaypoint(0, 700);
gamePath.addWaypoint(400, 700);
gamePath.addWaypoint(400, 1100);
gamePath.addWaypoint(1100, 1100);
gamePath.addWaypoint(1100, 1500);
gamePath.addWaypoint(400, 1500);
gamePath.addWaypoint(400, 1900);
gamePath.addWaypoint(1600, 1900);
gamePath.addWaypoint(1600, 700);
gamePath.addWaypoint(2048, 700);
// Render path
gamePath.render();
game.addChild(gamePath);
// Create Punchline (the end point to defend)
var punchline = LK.getAsset('punchline', {
anchorX: 0.5,
anchorY: 0.5
});
punchline.x = 2048 - 100;
punchline.y = 700;
game.addChild(punchline);
// Create build spots along the path
createBuildSpots();
}
function createBuildSpots() {
// Create build spots strategically around the path
var spots = [{
x: 300,
y: 550
}, {
x: 300,
y: 850
}, {
x: 550,
y: 1000
}, {
x: 550,
y: 1200
}, {
x: 950,
y: 1000
}, {
x: 950,
y: 1200
}, {
x: 950,
y: 1350
}, {
x: 950,
y: 1650
}, {
x: 550,
y: 1400
}, {
x: 550,
y: 1600
}, {
x: 300,
y: 1750
}, {
x: 300,
y: 2050
}, {
x: 550,
y: 1900
}, {
x: 800,
y: 1750
}, {
x: 800,
y: 2050
}, {
x: 1400,
y: 1750
}, {
x: 1400,
y: 2050
}, {
x: 1800,
y: 1900
}, {
x: 1450,
y: 850
}, {
x: 1450,
y: 550
}, {
x: 1800,
y: 700
}];
for (var i = 0; i < spots.length; i++) {
var spot = new BuildSpot();
spot.x = spots[i].x;
spot.y = spots[i].y;
buildSpots.push(spot);
game.addChild(spot);
}
}
// Define waves
function createWaves() {
// Wave 1: 10 normal enemies
waves.push({
enemies: [{
type: 'normal',
count: 10,
delay: 1.5,
difficulty: 1
}]
});
// Wave 2: Mix of normal and pun haters
waves.push({
enemies: [{
type: 'normal',
count: 8,
delay: 1.2,
difficulty: 1
}, {
type: 'punHater',
count: 5,
delay: 2,
difficulty: 1
}]
});
// Wave 3: Mix of all types
waves.push({
enemies: [{
type: 'normal',
count: 10,
delay: 1,
difficulty: 1.2
}, {
type: 'punHater',
count: 7,
delay: 1.5,
difficulty: 1.2
}, {
type: 'slapstickHater',
count: 7,
delay: 1.5,
difficulty: 1.2
}]
});
// Wave 4: More enemies, tougher
waves.push({
enemies: [{
type: 'normal',
count: 15,
delay: 0.8,
difficulty: 1.5
}, {
type: 'punHater',
count: 10,
delay: 1.2,
difficulty: 1.5
}, {
type: 'slapstickHater',
count: 10,
delay: 1.2,
difficulty: 1.5
}]
});
// Wave 5: Final wave, very tough
waves.push({
enemies: [{
type: 'normal',
count: 20,
delay: 0.7,
difficulty: 2
}, {
type: 'punHater',
count: 15,
delay: 1,
difficulty: 2
}, {
type: 'slapstickHater',
count: 15,
delay: 1,
difficulty: 2
}]
});
// Only update waveText if it has been initialized
if (waveText) {
waveText.setText("Wave: " + currentWave + "/" + waves.length);
}
}
// Start a wave
function startWave() {
if (waveInProgress || currentWave >= waves.length) {
return;
}
waveInProgress = true;
currentWave++;
waveText.setText("Wave: " + currentWave + "/" + waves.length);
nextWaveText.visible = false;
startWaveButton.visible = false;
var wave = waves[currentWave - 1];
var enemyGroups = wave.enemies;
var enemyTypesProcessed = 0;
function spawnEnemyGroup(groupIndex) {
if (groupIndex >= enemyGroups.length) {
enemyTypesProcessed++;
if (enemyTypesProcessed >= enemyGroups.length) {
// All enemy types processed, clear interval
LK.clearInterval(spawnInterval);
// Check for wave completion periodically
var checkInterval = LK.setInterval(function () {
if (enemies.length === 0) {
waveInProgress = false;
LK.clearInterval(checkInterval);
// Last wave completed
if (currentWave >= waves.length) {
// Player wins
LK.showYouWin();
} else {
// Prepare for next wave
nextWaveCountdown = 15;
nextWaveText.setText("Next wave: " + nextWaveCountdown);
nextWaveText.visible = true;
startWaveButton.visible = true;
// Bonus laughs for completing the wave
laughs += 50 + currentWave * 10;
laughsText.setText("Laughs: " + laughs);
}
}
}, 1000);
return;
}
// Move to next enemy type
groupIndex = 0;
}
var group = enemyGroups[groupIndex];
if (group.count > 0) {
// Spawn enemy
var enemy = new Enemy();
enemy.setup(group.type, group.difficulty);
enemy.x = gamePath.waypoints[0].x;
enemy.y = gamePath.waypoints[0].y;
game.addChild(enemy);
enemies.push(enemy);
// Decrease count
group.count--;
// Schedule next spawn for this group
LK.setTimeout(function () {
spawnEnemyGroup(groupIndex);
}, group.delay * 1000);
} else {
// Move to next group
spawnEnemyGroup(groupIndex + 1);
}
}
// Start spawning enemies
spawnInterval = LK.setInterval(function () {
spawnEnemyGroup(0);
}, 100);
}
// Initialize game
function initGame() {
// Create path
createPath();
// Create waves
createWaves();
// Init UI
initUI();
// Start countdown timer for first wave
var countdownInterval = LK.setInterval(function () {
if (!waveInProgress) {
nextWaveCountdown--;
nextWaveText.setText("Next wave: " + nextWaveCountdown);
if (nextWaveCountdown <= 0) {
LK.clearInterval(countdownInterval);
startWave();
}
}
}, 1000);
// Play background music
LK.playMusic('bgMusic', {
fade: {
start: 0,
end: 0.3,
duration: 1000
}
});
}
// Initialize game
initGame();
// Global event handlers
game.down = function (x, y, obj) {
// Hide build menu if clicking outside
if (buildMenu.visible) {
var clickedInMenu = false;
// Check if clicked on the menu
if (x >= buildMenu.x - 150 && x <= buildMenu.x + 150 && y >= buildMenu.y - 150 && y <= buildMenu.y + 150) {
clickedInMenu = true;
}
if (!clickedInMenu) {
hideBuildMenu();
}
}
// Hide tower info if clicking outside
if (selectedTower) {
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i] === obj) {
clickedOnTower = true;
break;
}
}
if (!clickedOnTower) {
if (selectedTower.showingRange) {
selectedTower.toggleRangeIndicator();
}
selectedTower = null;
upgradeButton.visible = false;
}
}
};
// Main game update
game.update = function () {
// Update all enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
// If enemy is dead but not yet removed
if (enemy.health <= 0) {
enemies.splice(i, 1);
enemy.destroy();
continue;
}
// If enemy reached the end
if (enemy.reachedEnd) {
enemies.splice(i, 1);
enemy.destroy();
continue;
}
}
// Update all projectiles
for (var j = projectiles.length - 1; j >= 0; j--) {
var projectile = projectiles[j];
// If projectile is missing
if (!projectile.parent) {
projectiles.splice(j, 1);
}
}
// Update UI button states
if (upgradeButton.visible && selectedTower) {
var upgradeCost = selectedTower.getUpgradeCost();
upgradeButton.tint = laughs >= upgradeCost ? 0xFFFFFF : 0xFF0000;
}
if (buildMenu.visible) {
normalTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
punTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
slapstickTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
knockKnockTowerButton.tint = laughs >= 10 ? 0xFFFFFF : 0xFF0000;
}
};
Happy person. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A path. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A speech bubble saying Haha!. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A door with the words Joking Around on it. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A boxing glove. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A bat holding a sign that says Jokes 4 Ever!. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A glove on a stick. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A button that says Start Wave. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A door. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows