User prompt
The Knock Knock tower can’t see the enemies...
User prompt
Finish it.
User prompt
Now the Knock Knock tower won’t become a wall...
User prompt
Make it so the enemies can’t go through the wall of Knock Knock towers.
User prompt
Make the Knock Knock tower become a wall by duplicating, not shooting.
User prompt
Make the Knock Knock tower copy itself until it’s a wall that the enemies can’t cross at all, can be placed on the track
User prompt
Put the start wave button on the map.
User prompt
Code the start wave button to start the next wave when you press it.
User prompt
Make it so the enemies don’t have so much health, also, make all the towers cost 10 laughs.
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'self.detachChildren();' Line Number: 335
User prompt
Make us start with 100 LaughPoints to start an army of jokes.
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'waveText.setText')' in or related to this line: 'waveText.setText("Wave: " + currentWave + "/" + waves.length);' Line Number: 854
Code edit (1 edits merged)
Please save this source code
User prompt
Joke Defense: Laugh or Lose
Initial prompt
A tower defense game where the towers are jokes.
/**** * 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