/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0, currency: 0 }); /**** * Classes ****/ // Ally: Semi-transparent melee ally that follows sniper and attacks nearest enemy var Ally = Container.expand(function () { var self = Container.call(this); // Use a unique ally soldier image as the ally's body self.graphics = self.attachAsset('allySoldier', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, scaleY: 1.2 }); self.graphics.alpha = 0.85; // Slightly transparent for distinction // Add shadow under ally (after graphics is created) self.shadow = new Shadow(); self.shadow.y = 48; var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.alpha = 0.5; self.addChildAt(self.shadow, 0); // In update, keep shadow under the character var _superUpdate = self.update; self.update = function () { // Shadow follows character if (self.shadow) { self.shadow.x = 0; self.shadow.y = 40; var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); } if (_superUpdate) _superUpdate.apply(self, arguments); }; self.maxHealth = 100; self.health = self.maxHealth; self.damage = 1; self.speed = 3.2; // Faster than regular enemy speed self.attackRange = 90; // Melee range self.attackCooldown = 600; // ms between attacks // Use regular enemy walk animation for Ally self.walkAnimTick = 0; self.lastMoveDirection = 1; self.updateWalkAnim = function () { // Diagonal walk animation: rotate left and right while moving if (self.active) { // Determine direction (toward target or following sniper) var dir = 1; if (self.targetEnemy) { dir = self.targetEnemy.x > self.x ? 1 : -1; } else if (sniper) { dir = sniper.x > self.x ? 1 : -1; } self.lastMoveDirection = dir; // Diagonal walk: oscillate rotation left and right as walking // Use a sine wave for smooth left-right rotation var walkOsc = Math.sin(LK.ticks * 0.18) * 0.28; // amplitude controls max angle (radians) tween(self.graphics, { rotation: walkOsc }, { duration: 180, easing: tween.easeInOut }); } // Add up/down bobbing for walk animation (like enemy) var walkCycle = Math.sin(LK.ticks * 0.15) * 6; self.graphics.y = walkCycle; }; self.lastAttack = 0; self.targetEnemy = null; self.followDistance = 80; // Distance to keep from sniper self.active = true; // Health bar background self.healthBarBg = LK.getAsset('bullet', { anchorX: 0.5, anchorY: 0.5, tint: 0x000000, scaleX: 1.6, scaleY: 0.22 }); self.healthBarBg.y = 60; self.addChild(self.healthBarBg); // Health bar fill self.healthBar = LK.getAsset('bullet', { anchorX: 0, anchorY: 0.5, tint: 0x00FF00, scaleX: 1.5, scaleY: 0.18 }); self.healthBar.y = 60; self.healthBar.x = -self.healthBarBg.width / 2; self.addChild(self.healthBar); // Update health bar self.updateHealthBar = function () { var percent = Math.max(0, self.health / self.maxHealth); self.healthBar.scale.x = 1.5 * percent; if (percent < 0.3) { self.healthBar.tint = 0xFF0000; } else if (percent < 0.6) { self.healthBar.tint = 0xFFFF00; } else { self.healthBar.tint = 0x00FF00; } self.healthBar.visible = percent < 1; self.healthBarBg.visible = percent < 1; }; // Take damage self.takeDamage = function (amount) { self.health -= amount; self.updateHealthBar(); LK.effects.flashObject(self, 0xff0000, 200); // Show floating damage number if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 40, amount, 0x00FFFF); } if (self.health <= 0) { self.active = false; tween(self, { alpha: 0 }, { duration: 400 }); self.visible = false; } }; // Ally update: follow sniper or attack nearest enemy self.update = function () { if (!self.active) return; // Prevent ally from dying if it moves off the top or sides of the screen if (self.x < 50) self.x = 50; if (self.x > 2048 - 50) self.x = 2048 - 50; if (self.y < -100) self.y = -100; self.updateHealthBar(); self.updateWalkAnim(); // Find nearest enemy anywhere on the map (no range limit) var nearest = null; var minDist = 99999; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (!e.active) continue; var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = e; } } var now = Date.now(); if (nearest) { // Always move toward nearest enemy if any exist self.targetEnemy = nearest; var angle = Math.atan2(nearest.y - self.y, nearest.x - self.x); // If not in attack range, move toward enemy if (minDist > self.attackRange * 0.7) { self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } // Attack if in range and cooldown ready if (minDist <= self.attackRange && now - self.lastAttack > self.attackCooldown) { self.lastAttack = now; // 10% chance to stun, 10% chance to slow, 5% chance to confuse, 10% crit var isCrit = Math.random() < 0.10; var effectRoll = Math.random(); if (isCrit) { nearest.takeDamage(self.damage * 2, 'basic', true); } else if (effectRoll < 0.10) { nearest.takeDamage(self.damage, 'stun'); } else if (effectRoll < 0.20) { nearest.takeDamage(self.damage, 'slow'); } else if (effectRoll < 0.25) { nearest.takeDamage(self.damage, 'confuse'); } else { nearest.takeDamage(self.damage, 'basic'); } LK.effects.flashObject(nearest, 0x00FFFF, 120); } } else { // No enemy present, follow sniper self.targetEnemy = null; var dx = sniper.x - self.x; var dy = sniper.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > self.followDistance) { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed * 0.7; self.y += Math.sin(angle) * self.speed * 0.7; } } }; return self; }); // Bomb: Air-dropped bomb that bounces and explodes var Bomb = Container.expand(function () { var self = Container.call(this); // Bomb body (yellow ellipse) self.bombBody = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.2, scaleY: 2.2, tint: 0xFFD700 // Gold/yellow }); // Shadow self.shadow = LK.getAsset('bullet', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.2, scaleY: 0.5, tint: 0x000000 }); self.shadow.alpha = 0.35; self.shadow.y = self.bombBody.height * 1.1; self.addChild(self.shadow); self.vy = 0; // vertical speed self.gravity = 2.2; // gravity self.bounceCount = 0; self.maxBounces = 1; // Only bounce once self.bounced = false; self.exploded = false; self.groundY = null; // set on spawn // Explosion effect self.explode = function () { if (self.exploded) return; self.exploded = true; // Flash and scale up bomb tween(self.bombBody, { scaleX: 5, scaleY: 5, alpha: 0 }, { duration: 220, easing: tween.easeOut, onFinish: function onFinish() { self.visible = false; self.active = false; } }); // Flash screen LK.effects.flashScreen(0xFFFF00, 200); // Damage all enemies in radius var radius = 220; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (!e.active) continue; var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < radius) { e.takeDamage(20, 'basic', true); } } // Show explosion text showDamageNumber(self.x, self.y - 60, "BOOM", 0xFFD700, "CRIT"); }; self.update = function () { if (self.exploded) return; if (self.groundY === null) { // Set groundY to the first y where bomb lands (simulate ground) self.groundY = self.y + 320; } // Move bomb down self.y += self.vy; self.vy += self.gravity; // Shadow follows x, stays at groundY self.shadow.x = 0; self.shadow.y = self.bombBody.height * 1.1 + (self.groundY - self.y); // Bounce logic if (!self.bounced && self.y >= self.groundY) { self.bounced = true; self.bounceCount++; // Bounce up with less speed self.y = self.groundY; self.vy = -22; // Squash bomb for bounce tween(self.bombBody, { scaleY: 1.2, scaleX: 2.8 }, { duration: 80, yoyo: true, repeat: 1 }); // Play bounce sound/effect (optional) } else if (self.bounced && self.y >= self.groundY) { // Landed after bounce, explode self.y = self.groundY; self.vy = 0; self.explode(); } }; self.active = true; return self; }); var BuildingShop = Container.expand(function () { var self = Container.call(this); self.isOpen = false; self.buildings = [{ name: "Basic Wall", price: 10, health: 100, owned: false }, { name: "Reinforced Wall", price: 20, health: 200, owned: false }, { name: "Sniper Tower", price: 100, health: 300, damage: 2, fireRate: 2000, owned: false }]; // Shop button self.shopButton = new Text2("BUILDINGS", { size: 80, fill: 0xFFFF00 }); self.shopButton.anchor.set(0.5, 0); // Add background to make button more visible var buttonBg = LK.getAsset('bullet', { width: 300, height: 100, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); buttonBg.alpha = 0.8; buttonBg.y = self.shopButton.height / 2; self.addChild(buttonBg); self.addChild(self.shopButton); // Shop panel (hidden by default) self.panel = new Container(); self.panel.visible = false; self.addChild(self.panel); // Create building options self.buildingItems = []; for (var i = 0; i < self.buildings.length; i++) { var building = self.buildings[i]; var item = new Container(); var bg = LK.getAsset('bullet', { width: 400, height: 150, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); bg.alpha = 0.8; item.addChild(bg); var title = new Text2(building.name, { size: 40, fill: 0xFFFFFF }); title.anchor.set(0.5, 0); title.y = -50; item.addChild(title); var info; if (building.name === "Sniper Tower") { info = new Text2("Health: " + building.health + " | Damage: " + building.damage, { size: 30, fill: 0xFFFFFF }); } else { info = new Text2("Health: " + building.health, { size: 30, fill: 0xFFFFFF }); } info.anchor.set(0.5, 0); info.y = 0; item.addChild(info); var priceText = new Text2(building.owned ? "OWNED" : "$" + building.price, { size: 35, fill: building.owned ? 0x00FF00 : 0xFFFF00 }); priceText.anchor.set(0.5, 0); priceText.y = 40; item.addChild(priceText); item.y = i * 200; item.buildingIndex = i; self.buildingItems.push(item); self.panel.addChild(item); } // Position panel self.panel.y = 250; // Handle shop button press self.shopButton.down = function (x, y, obj) { self.toggleShop(); }; // Toggle shop visibility self.toggleShop = function () { self.isOpen = !self.isOpen; self.panel.visible = self.isOpen; }; // Handle building selection/purchase self.selectBuilding = function (index) { var building = self.buildings[index]; if (currency >= building.price) { // Always allow purchase, deduct currency every time currency -= building.price; updateUI(); // Update UI to show price (never "OWNED") var priceText = self.buildingItems[index].children[3]; priceText.setText("$" + building.price, { fill: 0xFFFF00 }); LK.getSound('upgrade').play(); // After purchase, immediately select it for placement var buildingType; var buildingHealth = building.health; if (index === 0) { buildingType = 'basic'; } else if (index === 1) { buildingType = 'reinforced'; } else if (index === 2) { buildingType = 'sniper'; } // Create and add placeable wall game.placeableWall = new PlaceableWall(buildingType, buildingHealth); game.addChild(game.placeableWall); game.isPlacing = true; // Close shop self.toggleShop(); return true; } else if (building.owned) { // (Legacy: If owned, allow placement without payment, but this should never be true now) var buildingType; var buildingHealth = building.health; if (index === 0) { buildingType = 'basic'; } else if (index === 1) { buildingType = 'reinforced'; } else if (index === 2) { buildingType = 'sniper'; } game.placeableWall = new PlaceableWall(buildingType, buildingHealth); game.addChild(game.placeableWall); game.isPlacing = true; self.toggleShop(); return true; } else { // Not enough currency LK.effects.flashScreen(0xFF0000, 300); return false; } }; // Check if an item was clicked self.checkItemClick = function (x, y) { if (!self.isOpen) return false; var pos = self.toLocal({ x: x, y: y }); for (var i = 0; i < self.buildingItems.length; i++) { var item = self.buildingItems[i]; // Use item's actual bounds for hit detection var bounds = item.children[0].getBounds(); // children[0] is the background // Convert bounds to local coordinates relative to the panel var itemLeft = item.x + bounds.x - bounds.width * item.children[0].anchorX; var itemRight = itemLeft + bounds.width; var itemTop = item.y + bounds.y - bounds.height * item.children[0].anchorY; var itemBottom = itemTop + bounds.height; if (pos.x >= itemLeft && pos.x <= itemRight && pos.y >= itemTop && pos.y <= itemBottom) { return self.selectBuilding(item.buildingIndex); } } return false; }; // Add down event handlers to each building item for (var i = 0; i < self.buildingItems.length; i++) { var item = self.buildingItems[i]; item.interactive = true; item.index = i; // Store the index // Using custom event handler for each item (function (index) { item.down = function (x, y, obj) { self.selectBuilding(index); }; })(i); } return self; }); var Bullet = Container.expand(function () { var self = Container.call(this); // No shadow for bullets self.graphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 15; // Default speed, will be overridden self.damage = 1; // Default damage, will be overridden self.active = true; self.update = function () { if (!self.active) return; // Use directional movement if speedX and speedY are defined if (self.speedX !== undefined && self.speedY !== undefined) { self.x += self.speedX; self.y += self.speedY; } else { // Fallback to original vertical-only movement self.y -= self.speed; } // Bullet goes off screen (check all edges) if (self.y < -50 || self.y > 2732 + 50 || self.x < -50 || self.x > 2048 + 50) { self.active = false; } }; self.hit = function () { self.active = false; LK.getSound('enemyHit').play(); LK.effects.flashObject(self, 0xffffff, 200); }; return self; }); var Bunker = Container.expand(function () { var self = Container.call(this); // Add shadow under bunker self.shadow = new Shadow(); self.shadow.y = 60; self.addChildAt(self.shadow, 0); if (self.graphics) { var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.scale.x = baseWidth * 0.8 / 100; self.shadow.graphics.alpha = 0.5; } self.graphics = self.attachAsset('bunker', { anchorX: 0.5, anchorY: 0.5 }); self.damageIndicator = self.attachAsset('damageIndicator', { anchorX: 0.5, anchorY: 0.5 }); self.damageIndicator.alpha = 0; self.damageIndicator.originalX = 0; self.damageIndicator.originalY = 0; self.showDamage = function (percentage) { // Make indicator more visible as health decreases self.damageIndicator.alpha = 1 - percentage; // Move the indicator based on health percentage // Lower health = more movement var moveFactor = (1 - percentage) * 10; self.damageIndicator.x = self.damageIndicator.originalX + (Math.random() * 2 - 1) * moveFactor; self.damageIndicator.y = self.damageIndicator.originalY + (Math.random() * 2 - 1) * moveFactor; // Change tint to become redder as health decreases var healthColor = Math.floor(percentage * 255); self.damageIndicator.tint = 255 << 16 | healthColor << 8 | healthColor; }; // Store original position self.onAddedToStage = function () { self.damageIndicator.originalX = self.damageIndicator.x; self.damageIndicator.originalY = self.damageIndicator.y; }; return self; }); var Bush = Container.expand(function () { var self = Container.call(this); // Random bush appearance with slight variation var type = Math.floor(Math.random() * 3); var size = Math.random() * 0.5 + 0.8; // Size between 0.8 and 1.3 var rotation = Math.random() * 0.3 - 0.15; // Slight rotation // Create the bush using bullet shape with green tint self.graphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, tint: 0x2E8B57, // Sea green color scaleX: size, scaleY: size }); // Apply random rotation self.graphics.rotation = rotation; // Add some details to make bushes look different from each other if (type === 0) { // First type - taller bush self.graphics.scale.y *= 1.3; self.graphics.tint = 0x228B22; // Forest green } else if (type === 1) { // Second type - wider bush self.graphics.scale.x *= 1.2; self.graphics.tint = 0x006400; // Dark green } else { // Third type - round bush self.graphics.tint = 0x3CB371; // Medium sea green } return self; }); var Enemy = Container.expand(function (type) { var self = Container.call(this); self.type = type || 'regular'; // Set properties based on enemy type switch (self.type) { case 'fast': self.graphics = self.attachAsset('fastEnemy', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.5, scaleY: 2.5 }); self.speed = 3; self.hp = 1; self.damage = 10; self.points = 15; self.currency = 2; break; case 'tank': self.graphics = self.attachAsset('tankEnemy', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.5, scaleY: 2.5 }); self.speed = 1; self.hp = 5; self.damage = 25; self.points = 30; self.currency = 5; break; default: // regular self.graphics = self.attachAsset('regularEnemy', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.5, scaleY: 2.5 }); self.speed = 2; self.hp = 2; self.damage = 15; self.points = 10; self.currency = 1; } // Create and add shadow beneath enemy (after graphics is created) self.shadow = new Shadow(); // Position shadow further in front of enemy, and a bit more forward self.shadow.y = self.graphics.height * 0.62; // More in front var baseWidth = self.graphics.width * self.graphics.scale.x; if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.alpha = 0.5; self.addChildAt(self.shadow, 0); // Add shadow behind the enemy // In update, keep shadow under the character var _superUpdate = self.update; self.update = function () { // Shadow always follows enemy, not walk animation if (self.shadow) { self.shadow.x = 0; self.shadow.y = self.graphics.height * 0.62; // More in front var baseWidth = self.graphics.width * self.graphics.scale.x; if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); } if (_superUpdate) _superUpdate.apply(self, arguments); }; self.active = true; self.update = function () { if (!self.active) return; // --- Begin: Status effect processing --- // Burning: take 1 damage every 400ms while burning if (self.burningUntil && Date.now() < self.burningUntil) { if (!self.burnTick) self.burnTick = Date.now(); if (Date.now() - self.burnTick > 400) { self.burnTick = Date.now(); self.hp -= 1; if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 40, 1, 0xFF6600); } LK.effects.flashObject(self, 0xFF6600, 100); if (self.hp <= 0) { self.die(); return; } } } else if (self.burningUntil && Date.now() >= self.burningUntil) { self.burningUntil = null; self.burnTick = null; } // Poison: take 1 damage every 600ms while poisoned if (self.poisonedUntil && Date.now() < self.poisonedUntil) { if (!self.poisonTick) self.poisonTick = Date.now(); if (Date.now() - self.poisonTick > 600) { self.poisonTick = Date.now(); self.hp -= 1; if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 40, 1, 0x00FF66); } LK.effects.flashObject(self, 0x00FF66, 100); if (self.hp <= 0) { self.die(); return; } } } else if (self.poisonedUntil && Date.now() >= self.poisonedUntil) { self.poisonedUntil = null; self.poisonTick = null; } // Shield: show visual effect if shielded if (self.hasShield && self.graphics) { if (!self.shieldGlow) { self.shieldGlow = LK.getAsset('bullet', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.8, scaleY: 2.8, tint: 0xFFFF00 }); self.shieldGlow.alpha = 0.25; self.addChildAt(self.shieldGlow, 1); } } else if (self.shieldGlow) { self.removeChild(self.shieldGlow); self.shieldGlow = null; } // --- End: Status effect processing --- // --- Status effect handling: stun, slow, confuse --- var now = Date.now(); if (self.stunnedUntil && now < self.stunnedUntil) { // Stunned: skip all movement and attack, but allow animation if (self.graphics) { tween(self.graphics, { rotation: 0 }, { duration: 120 }); } // Still allow walk animation for visual feedback var walkCycle = Math.sin(LK.ticks * 0.15) * 6; self.graphics.y = walkCycle; return; } else if (self.stunnedUntil && now >= self.stunnedUntil) { self.stunnedUntil = null; } if (self.slowedUntil && now < self.slowedUntil) { self.speedModifier = self.slowFactor || 0.5; } else if (self.slowedUntil && now >= self.slowedUntil) { self.slowedUntil = null; self.speedModifier = 1; } if (self.confusedUntil && now < self.confusedUntil) { // Randomize move direction if (Math.random() < 0.1) { self.moveDirection = Math.random() > 0.5 ? 1 : -1; } } else if (self.confusedUntil && now >= self.confusedUntil) { self.confusedUntil = null; } // Default speed modifier if (typeof self.speedModifier !== "number") self.speedModifier = 1; // Armored vehicle animation: switch asset for all 8 directions (including diagonals) if (self.type === "armoredVehicle" && self.graphics) { // Prevent asset switch if _preventAssetSwitch is set (e.g. during damage flash) if (self._preventAssetSwitch) { // Still update lastX/lastY for correct direction after flash self.lastX = self.x; self.lastY = self.y; } else { // Calculate movement direction var dx = 0, dy = 0; if (typeof self.lastX === "number" && typeof self.lastY === "number") { dx = self.x - self.lastX; dy = self.y - self.lastY; } // Only update if moved enough to change direction // Only switch asset if direction meaningfully changed (not for tiny dx/dy) var minMoveDist = 2.5; // Only consider direction change if moved at least this much if (Math.abs(dx) > minMoveDist || Math.abs(dy) > minMoveDist) { var dirAsset = "armoredVehicle_front"; // Calculate angle in degrees for direction var angle = Math.atan2(dy, dx) * 180 / Math.PI; // Normalize angle to [0, 360) if (angle < 0) angle += 360; // 8-directional asset selection if (angle >= 337.5 || angle < 22.5) { dirAsset = "armoredVehicle_right"; } else if (angle >= 22.5 && angle < 67.5) { dirAsset = "armoredVehicle_frontRight"; } else if (angle >= 67.5 && angle < 112.5) { dirAsset = "armoredVehicle_front"; } else if (angle >= 112.5 && angle < 157.5) { dirAsset = "armoredVehicle_frontLeft"; } else if (angle >= 157.5 && angle < 202.5) { dirAsset = "armoredVehicle_left"; } else if (angle >= 202.5 && angle < 247.5) { dirAsset = "armoredVehicle_backLeft"; } else if (angle >= 247.5 && angle < 292.5) { dirAsset = "armoredVehicle_back"; } else if (angle >= 292.5 && angle < 337.5) { dirAsset = "armoredVehicle_backRight"; } // Always use 'front' asset if moving almost straight down (dy > 0, dx is small) if (Math.abs(dx) < minMoveDist && dy > minMoveDist) { dirAsset = "armoredVehicle_front"; } // Only change asset if needed (and only if direction changed) if (!self.graphics.assetId || self.graphics.assetId !== dirAsset) { // Remove old graphics self.removeChild(self.graphics); self.graphics = self.attachAsset(dirAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7 }); // Always set assetId to prevent asset loss self.graphics.assetId = dirAsset; // Ensure shadow stays behind if (self.shadow) { self.setChildIndex(self.shadow, 0); } } // Save last direction asset for next frame self._lastArmoredVehicleAsset = dirAsset; } // Save last position for next frame self.lastX = self.x; self.lastY = self.y; } } // Basic diagonal movement pattern if (!self.moveDirection) { self.moveDirection = Math.random() > 0.5 ? 1 : -1; // Random initial direction self.moveCounter = 0; self.maxMoveDistance = Math.random() * 30 + 20; // Random move distance self.isAttackingWall = false; self.attackTarget = null; self.attackAnimationTicks = 0; } // If enemy is attacking a wall or ally, perform attack animation if (self.isAttackingWall && self.attackTarget) { // --- Always run walk animation, even while attacking --- // Diagonal walk: oscillate rotation left and right as walking (like Ally) var walkOsc = Math.sin(LK.ticks * 0.18) * 0.28; // Always reset scale to default before tween to prevent cumulative shrinking self.graphics.scale.x = self.type === 'fast' || self.type === 'tank' || self.type === 'regular' ? 2.5 : self.graphics.scale.x; self.graphics.scale.y = self.type === 'fast' || self.type === 'tank' || self.type === 'regular' ? 2.5 : self.graphics.scale.y; tween(self.graphics, { rotation: walkOsc }, { duration: 180, easing: tween.easeInOut }); // Add up/down bobbing for walk animation var walkCycle = Math.sin(LK.ticks * 0.15) * 6; self.graphics.y = walkCycle; // --- Ava fix: If being tweened (knockback), do not freeze in place --- // If a knockback tween is active, allow movement to resume after knockback ends if (self._knockbackTweenActive) { // Wait for knockback tween to finish before resuming attack animation return; } // Increment animation counter self.attackAnimationTicks++; // Every 45 frames complete a full attack cycle if (self.attackAnimationTicks < 20) { // Pull back for attack - move head backwards if (self.attackAnimationTicks === 1) { tween(self.graphics, { rotation: -0.3, y: -10 }, { duration: 300, easing: tween.easeOut }); } } else if (self.attackAnimationTicks < 25) { // Forward strike - move head forward quickly if (self.attackAnimationTicks === 20) { tween(self.graphics, { rotation: 0.2, y: 10 }, { duration: 150, easing: tween.easeIn }); } } else { // Reset animation counter and damage the wall or ally self.attackAnimationTicks = 0; // Apply damage to wall or ally if (self.attackTarget) { if (self.attackTarget.takeDamage) { self.attackTarget.takeDamage(self.damage / 5); LK.effects.flashObject(self.attackTarget, 0xFF0000, 200); } } } // When wall or ally is destroyed, resume movement if (!self.attackTarget || self.attackTarget.health !== undefined && self.attackTarget.health <= 0 || self.attackTarget.active !== undefined && self.attackTarget.active === false) { self.isAttackingWall = false; self.attackTarget = null; // Reset position and rotation tween(self.graphics, { rotation: 0, y: 0 }, { duration: 200, easing: tween.easeOut }); } return; // Don't move while attacking } // --- Begin: Sniper Tower targeting logic --- // Priority: Attack sniper tower if present and in range, else ally, else bunker var sniperTowerWall = null; var minTowerDist = 99999; if (typeof game !== "undefined" && game.walls && game.walls.length > 0) { for (var i = 0; i < game.walls.length; i++) { var wall = game.walls[i]; if (wall.type === "sniper" && wall.health > 0) { var dxTower = wall.x - self.x; var dyTower = wall.y - self.y; var distTower = Math.sqrt(dxTower * dxTower + dyTower * dyTower); if (distTower < minTowerDist) { minTowerDist = distTower; sniperTowerWall = wall; } } } } if (sniperTowerWall) { // Move toward sniper tower and attack if in range var dxTower = sniperTowerWall.x - self.x; var dyTower = sniperTowerWall.y - self.y; var distTower = Math.sqrt(dxTower * dxTower + dyTower * dyTower); // --- FIX: Armored vehicle must attack only from close range, not from distance --- var attackRange = 60; if (self.type === "armoredVehicle") { attackRange = 60; // Same as other enemies, no ranged attack } if (distTower < attackRange) { if (!self.isAttackingWall || self.attackTarget !== sniperTowerWall) { self.isAttackingWall = true; self.attackTarget = sniperTowerWall; self.attackAnimationTicks = 0; } // Do not move if attacking tower return; } else { // Move toward sniper tower if not in attack range var angleToTower = Math.atan2(dyTower, dxTower); self.x += Math.cos(angleToTower) * self.speed; self.y += Math.sin(angleToTower) * self.speed; // Add walking animation for following tower if (LK.ticks % 10 === 0 && !self.isAttackingWall) { tween(self.graphics, { rotation: (dxTower > 0 ? 1 : -1) * 0.1 }, { duration: 250, easing: tween.easeInOut }); } return; } } // --- End: Sniper Tower targeting logic --- // Check for ally in range and attack if present and active if (typeof ally !== "undefined" && ally && ally.active) { var dxAlly = ally.x - self.x; var dyAlly = ally.y - self.y; var distAlly = Math.sqrt(dxAlly * dxAlly + dyAlly * dyAlly); // If ally is present, always follow and attack ally if (distAlly < 60) { if (!self.isAttackingWall || self.attackTarget !== ally) { self.isAttackingWall = true; self.attackTarget = ally; self.attackAnimationTicks = 0; } // Do not move if attacking ally return; } else { // Move toward ally if not in attack range var angleToAlly = Math.atan2(dyAlly, dxAlly); self.x += Math.cos(angleToAlly) * self.speed; self.y += Math.sin(angleToAlly) * self.speed; // Add walking animation for following ally if (LK.ticks % 10 === 0 && !self.isAttackingWall) { tween(self.graphics, { rotation: (dxAlly > 0 ? 1 : -1) * 0.1 }, { duration: 250, easing: tween.easeInOut }); } return; } } // Move diagonally self.y += self.speed; self.x += self.moveDirection * (self.speed * 0.5); // Switch direction after a certain distance self.moveCounter += self.speed; if (self.moveCounter >= self.maxMoveDistance) { self.moveDirection *= -1; // Reverse direction self.moveCounter = 0; self.maxMoveDistance = Math.random() * 30 + 20; // New random distance } // Make the shadow grow or shrink slightly with height simulation // Calculate height simulation based on movement cycle var heightSimulation = Math.sin(LK.ticks * 0.05) * 0.1; // Update shadow properties to create floating effect self.shadow.graphics.alpha = 0.5 - heightSimulation * 0.1; // Vary opacity with more baseline visibility self.shadow.graphics.scale.x = self.graphics.width * self.graphics.scale.x * 0.8 / 100 * (1 - heightSimulation); // Vary size // Ensure shadow follows enemy even with movement self.shadow.x = 0; // Add walking animation (always run, not just every 10 ticks, to prevent animation freeze) if (!self.isAttackingWall) { // Only apply walk animation to non-armoredVehicle types if (self.type === 'fast' || self.type === 'tank' || self.type === 'regular') { // Diagonal walk: oscillate rotation left and right as walking (like Ally) var walkOsc = Math.sin(LK.ticks * 0.18) * 0.28; // Always reset scale to default before tween to prevent cumulative shrinking self.graphics.scale.x = 2.5; self.graphics.scale.y = 2.5; tween(self.graphics, { rotation: walkOsc }, { duration: 180, easing: tween.easeInOut }); // Add up/down bobbing for walk animation var walkCycle = Math.sin(LK.ticks * 0.15) * 6; self.graphics.y = walkCycle; } // For armoredVehicle, do not animate walk at all (no rotation, no bobbing) } // Ensure enemy stays within screen bounds (but do NOT kill enemy if off screen, just clamp position) if (self.x < 50) { self.x = 50; self.moveDirection = 1; } else if (self.x > 2048 - 50) { self.x = 2048 - 50; self.moveDirection = -1; } // Prevent enemy from dying if it moves off the top or sides of the screen if (self.y < -100) { self.y = -100; } // Check if enemy reached the bunker if (self.y > bunkerY - 50) { self.attackBunker(); } }; self.takeDamage = function (amount, weaponType, isCritical) { // --- Begin: Advanced status effects and reactions --- weaponType = weaponType || 'basic'; isCritical = !!isCritical; // Burning: continuous damage over time, red flash if (weaponType === 'burn' && !self.burningUntil) { self.burningUntil = Date.now() + 2500; self.burnTick = Date.now(); LK.effects.flashObject(self, 0xFF6600, 200); } // Freezing: slow and blue flash if (weaponType === 'freeze') { self.slowedUntil = Date.now() + 2500; self.slowFactor = 0.3; LK.effects.flashObject(self, 0x66CCFF, 200); } // Poison: green flash, damage over time if (weaponType === 'poison' && !self.poisonedUntil) { self.poisonedUntil = Date.now() + 4000; self.poisonTick = Date.now(); LK.effects.flashObject(self, 0x00FF66, 200); } // Shield: absorb one hit, yellow flash if (self.hasShield) { self.hasShield = false; LK.effects.flashObject(self, 0xFFFF00, 400); // Show floating shield break if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 60, "SHIELD", 0xFFFF00); } return; } // --- End: Advanced status effects and reactions --- // Default weaponType to 'basic' if not provided weaponType = weaponType || 'basic'; isCritical = !!isCritical; // Critical hit: 2x damage, special effect if (isCritical) { amount = Math.floor(amount * 2); if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 60, amount, 0xFFD700); // Gold color for crit } LK.effects.flashObject(self, 0xFFD700, 350); // Briefly scale up enemy for crit tween(self.graphics, { scaleX: self.type === 'armoredVehicle' ? 1.7 : self.graphics.scale.x * 1.3, scaleY: self.type === 'armoredVehicle' ? 1.7 : self.graphics.scale.y * 1.3 }, { duration: 80, yoyo: true, repeat: 1, onFinish: function onFinish() { if (self.type === 'fast' || self.type === 'tank' || self.type === 'regular') { self.graphics.scale.x = 2.5; self.graphics.scale.y = 2.5; } else if (self.type === 'armoredVehicle') { self.graphics.scale.x = 1.7; self.graphics.scale.y = 1.7; } } }); } self.hp -= amount; // Show floating damage number if (!isCritical && typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 40, amount, 0xFF2222); } // --- Weapon-specific hit reactions --- if (self.hp > 0) { // Rifle: 10% chance to briefly stun if (weaponType === 'basic' && Math.random() < 0.10) { self.stunnedUntil = Date.now() + 350; LK.effects.flashObject(self, 0x00FFFF, 120); } // Sniper: always knockback if (weaponType === 'sniper') { var angle = Math.atan2(self.y - sniper.y, self.x - sniper.x); var knockbackDist = 60 + Math.random() * 30; var targetX = self.x + Math.cos(angle) * knockbackDist; var targetY = self.y + Math.sin(angle) * knockbackDist; // Clamp to screen targetX = Math.max(50, Math.min(2048 - 50, targetX)); targetY = Math.max(0, Math.min(2732 - 50, targetY)); // --- Ava fix: Set knockback tween flag so update() can allow movement after knockback --- // Only apply knockback tween if not armoredVehicle, armoredVehicle just flashes if (self.type === "armoredVehicle") { // Only flash, do not hide or move, and do NOT remove/re-add graphics to prevent flicker if (self.graphics) { // Temporarily set a flag to prevent asset switching in update during flash self._preventAssetSwitch = true; LK.effects.flashObject(self, 0x33CCFF, 180); // Remove the flag after flash duration LK.setTimeout(function () { self._preventAssetSwitch = false; }, 180); } } else { self._knockbackTweenActive = true; tween(self, { x: targetX, y: targetY }, { duration: 180, easing: tween.easeOut, onFinish: function onFinish() { self._knockbackTweenActive = false; } }); // Briefly flash blue LK.effects.flashObject(self, 0x33CCFF, 180); } } // Super Sniper: stun and strong knockback if (weaponType === 'super') { var angle = Math.atan2(self.y - sniper.y, self.x - sniper.x); var knockbackDist = 120 + Math.random() * 40; var targetX = self.x + Math.cos(angle) * knockbackDist; var targetY = self.y + Math.sin(angle) * knockbackDist; targetX = Math.max(50, Math.min(2048 - 50, targetX)); targetY = Math.max(0, Math.min(2732 - 50, targetY)); self._knockbackTweenActive = true; tween(self, { x: targetX, y: targetY }, { duration: 220, easing: tween.easeOut, onFinish: function onFinish() { self._knockbackTweenActive = false; } }); self.stunnedUntil = Date.now() + 600; LK.effects.flashObject(self, 0xFF00FF, 220); } // Tank: 20% chance to slow enemy for 2 seconds if (weaponType === 'tank' && Math.random() < 0.20) { self.slowedUntil = Date.now() + 2000; self.slowFactor = 0.5; LK.effects.flashObject(self, 0x00FF00, 200); } // Fast: 10% chance to confuse (random direction) for 1s if (weaponType === 'fast' && Math.random() < 0.10) { self.confusedUntil = Date.now() + 1000; LK.effects.flashObject(self, 0xFF00FF, 120); } // Burn: apply burning status if (weaponType === 'burn') { self.burningUntil = Date.now() + 2500; self.burnTick = Date.now(); if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 60, "BURN", 0xFF6600); } LK.effects.flashObject(self, 0xFF6600, 200); } // Freeze: apply slow if (weaponType === 'freeze') { self.slowedUntil = Date.now() + 2500; self.slowFactor = 0.3; if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 60, "FREEZE", 0x66CCFF); } LK.effects.flashObject(self, 0x66CCFF, 200); } // Poison: apply poison status if (weaponType === 'poison') { self.poisonedUntil = Date.now() + 4000; self.poisonTick = Date.now(); if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 60, "POISON", 0x00FF66); } LK.effects.flashObject(self, 0x00FF66, 200); } // ShieldPierce: remove shield if present if (weaponType === 'shieldPierce' && self.hasShield) { self.hasShield = false; if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 60, "SHIELD BREAK", 0xFFFF00); } LK.effects.flashObject(self, 0xFFFF00, 400); } // Default flash if (!isCritical && weaponType === 'basic') { LK.effects.flashObject(self, 0xffffff, 200); } } // --- End weapon-specific hit reactions --- if (self.hp <= 0) { self.die(); } }; self.die = function () { // Fade out shadow when enemy dies tween(self.shadow.graphics, { alpha: 0 }, { duration: 300 }); // Rotate enemy to make it lie on its side (90 degrees in radians) tween(self.graphics, { rotation: Math.PI / 2, alpha: 0.2 }, { duration: 500, easing: tween.easeOut }); // After rotation, fade out completely LK.setTimeout(function () { if (self.graphics) { tween(self.graphics, { alpha: 0 }, { duration: 800, easing: tween.easeIn }); } }, 400); self.active = false; var currentScore = LK.getScore(); var scoreMultiplier = 1; // Increase rewards based on score progression if (currentScore > 300) { scoreMultiplier = 2.5; } else if (currentScore > 200) { scoreMultiplier = 2; } else if (currentScore > 100) { scoreMultiplier = 1.5; } LK.setScore(currentScore + self.points); // Set different currency values based on enemy type with score multiplier if (self.type === 'regular') { currency += Math.ceil((3 + 2) * scoreMultiplier); } else if (self.type === 'fast') { currency += Math.ceil((4 + 2) * scoreMultiplier); } else if (self.type === 'tank') { currency += Math.ceil((6 + 2) * scoreMultiplier); } else { currency += Math.ceil((self.currency + 2) * scoreMultiplier); // Fallback } updateUI(); }; self.startAttackingWall = function (wall) { self.isAttackingWall = true; self.attackTarget = wall; self.attackAnimationTicks = 0; }; self.attackBunker = function () { bunkerHealth -= self.damage; updateBunkerHealth(); LK.getSound('bunkerHit').play(); LK.effects.flashObject(bunker, 0xff0000, 500); // Show floating damage number on bunker if (typeof showDamageNumber === "function") { showDamageNumber(bunker.x, bunker.y - 80, self.damage, 0xFF8800); } // Make damage indicator visible when taking damage bunker.damageIndicator.alpha = 1; self.active = false; // Check if bunker is destroyed if (bunkerHealth <= 0) { gameOver(); } }; return self; }); var MuzzleFlash = Container.expand(function () { var self = Container.call(this); // Add shadow under muzzle flash self.shadow = new Shadow(); self.shadow.y = 8; self.addChildAt(self.shadow, 0); if (self.graphics) { var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.scale.x = baseWidth * 0.8 / 100; self.shadow.graphics.alpha = 0.5; } // Create the flash using bullet shape with yellow-white tint self.graphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, tint: 0xFFFF99 // Bright yellow-white color for flash }); // Initially scale small self.graphics.scale.set(0.3, 0.3); self.graphics.alpha = 0.9; // Animation lifetime self.duration = 100; // ms self.startTime = 0; self.active = false; // Start the flash effect self.flash = function () { self.active = true; self.startTime = Date.now(); self.visible = true; // Reset and start animation self.graphics.scale.set(0.8, 0.8); self.graphics.alpha = 1; }; // Update the flash animation self.update = function () { if (!self.active) return; var elapsed = Date.now() - self.startTime; var progress = elapsed / self.duration; if (progress >= 1) { // Animation complete self.active = false; self.visible = false; return; } // Animate scale and alpha self.graphics.scale.set(0.8 * (1 - progress), 0.8 * (1 - progress)); self.graphics.alpha = 1 - progress; }; // Hide initially self.visible = false; return self; }); var PlaceableWall = Container.expand(function (buildingType, buildingHealth) { var self = Container.call(this); // Add shadow under placeable wall self.shadow = new Shadow(); self.shadow.y = 60; self.addChildAt(self.shadow, 0); if (self.graphics) { var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.scale.x = baseWidth * 0.8 / 100; self.shadow.graphics.alpha = 0.5; } // Create semi-transparent wall based on type if (buildingType === 'reinforced') { self.graphics = self.attachAsset('reinforcedWall', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else if (buildingType === 'sniper') { // Create a tower self.graphics = self.attachAsset('sniperTower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); // No need to add extra sniper on top since it's included in the image } else { // Basic wall self.graphics = self.attachAsset('basicWall', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } // Make it semi-transparent self.alpha = 0.7; // Store building properties for when it's placed self.buildingType = buildingType; self.buildingHealth = buildingHealth; return self; }); // Security: Melee security guard with walk animation (like Ally/Enemy) var Security = Container.expand(function () { var self = Container.call(this); // Add shadow under security self.shadow = new Shadow(); self.shadow.y = 48; self.addChildAt(self.shadow, 0); if (self.graphics) { var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.alpha = 0.5; } // Use ally soldier image for security for now (replace with unique asset if available) self.graphics = self.attachAsset('allySoldier', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, scaleY: 1.2 }); self.graphics.alpha = 1; self.maxHealth = 120; self.health = self.maxHealth; self.damage = 2; self.speed = 2.8; self.attackRange = 90; self.attackCooldown = 700; self.walkAnimTick = 0; self.lastMoveDirection = 1; self.updateWalkAnim = function () { // Simulate walk animation (same as Ally/Enemy) if (LK.ticks % 10 === 0 && self.active) { var dir = 1; if (self.targetEnemy) { dir = self.targetEnemy.x > self.x ? 1 : -1; } self.lastMoveDirection = dir; tween(self.graphics, { rotation: dir * 0.1 }, { duration: 250, easing: tween.easeInOut }); } var walkCycle = Math.sin(LK.ticks * 0.15) * 6; self.graphics.y = walkCycle; }; self.lastAttack = 0; self.targetEnemy = null; self.followDistance = 100; self.active = true; // Health bar background self.healthBarBg = LK.getAsset('bullet', { anchorX: 0.5, anchorY: 0.5, tint: 0x000000, scaleX: 1.6, scaleY: 0.22 }); self.healthBarBg.y = 60; self.addChild(self.healthBarBg); // Health bar fill self.healthBar = LK.getAsset('bullet', { anchorX: 0, anchorY: 0.5, tint: 0x00FF00, scaleX: 1.5, scaleY: 0.18 }); self.healthBar.y = 60; self.healthBar.x = -self.healthBarBg.width / 2; self.addChild(self.healthBar); // Update health bar self.updateHealthBar = function () { var percent = Math.max(0, self.health / self.maxHealth); self.healthBar.scale.x = 1.5 * percent; if (percent < 0.3) { self.healthBar.tint = 0xFF0000; } else if (percent < 0.6) { self.healthBar.tint = 0xFFFF00; } else { self.healthBar.tint = 0x00FF00; } self.healthBar.visible = percent < 1; self.healthBarBg.visible = percent < 1; }; // Take damage self.takeDamage = function (amount) { self.health -= amount; self.updateHealthBar(); LK.effects.flashObject(self, 0xff0000, 200); // Show floating damage number if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 40, amount, 0x00FFFF); } if (self.health <= 0) { self.active = false; tween(self, { alpha: 0 }, { duration: 400 }); self.visible = false; } }; // Security update: follow sniper or attack nearest enemy self.update = function () { if (!self.active) return; // Prevent security from dying if it moves off the top or sides of the screen if (self.x < 50) self.x = 50; if (self.x > 2048 - 50) self.x = 2048 - 50; if (self.y < -100) self.y = -100; self.updateHealthBar(); self.updateWalkAnim(); // Find nearest enemy in range var nearest = null; var minDist = 99999; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (!e.active) continue; var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = e; } } var now = Date.now(); if (nearest) { self.targetEnemy = nearest; var angle = Math.atan2(nearest.y - self.y, nearest.x - self.x); if (minDist > self.attackRange * 0.7) { self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } if (minDist <= self.attackRange && now - self.lastAttack > self.attackCooldown) { self.lastAttack = now; // 10% chance to stun, 10% chance to slow, 5% chance to confuse, 10% crit var isCrit = Math.random() < 0.10; var effectRoll = Math.random(); if (isCrit) { nearest.takeDamage(self.damage * 2, 'basic', true); } else if (effectRoll < 0.10) { nearest.takeDamage(self.damage, 'stun'); } else if (effectRoll < 0.20) { nearest.takeDamage(self.damage, 'slow'); } else if (effectRoll < 0.25) { nearest.takeDamage(self.damage, 'confuse'); } else { nearest.takeDamage(self.damage, 'basic'); } LK.effects.flashObject(nearest, 0x00FFFF, 120); } } else { self.targetEnemy = null; var dx = sniper.x - self.x; var dy = sniper.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > self.followDistance) { var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed * 0.7; self.y += Math.sin(angle) * self.speed * 0.7; } } }; return self; }); var Shadow = Container.expand(function () { var self = Container.call(this); // Create shadow using dedicated shadow asset self.graphics = self.attachAsset('shadow', { anchorX: 0.5, anchorY: 0.5 }); // Set shadow properties self.graphics.alpha = 0.5; // More visible shadow // Method to update shadow size based on parent self.updateSize = function (parentWidth) { // Make shadow larger and rounder self.graphics.scale.x = parentWidth * 1.1 / 100; self.graphics.scale.y = 0.7; // More circular/ellipse }; // Default scale for initial appearance self.graphics.scale.x = 1.1; self.graphics.scale.y = 0.7; return self; }); var Sniper = Container.expand(function () { var self = Container.call(this); // Add shadow under sniper self.shadow = new Shadow(); self.shadow.y = 40; self.addChildAt(self.shadow, 0); if (self.graphics) { var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.scale.x = baseWidth * 0.8 / 100; self.shadow.graphics.alpha = 0.5; } self.graphics = self.attachAsset('sniper', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); // Add rifle that will rotate toward cursor self.rifle = self.attachAsset('rifle', { anchorX: 0, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7 }); // Create references to all weapon graphics but only show the active one self.weapons = { basic: self.rifle, sniper: LK.getAsset('sniper_rifle', { anchorX: 0, anchorY: 0.5, scaleX: 1.2, scaleY: 1.2 }), "super": LK.getAsset('sniper_rifle', { anchorX: 0, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5, tint: 0xFF00FF // Magenta tint for Super Sniper }) }; // Add other weapons but hide them initially self.weapons.sniper.visible = false; self.weapons.sniper.x = 10; self.addChild(self.weapons.sniper); self.weapons["super"].visible = false; self.weapons["super"].x = 10; self.addChild(self.weapons["super"]); // Position rifle to appear like it's being held by the sniper self.rifle.x = 10; // Initialize weapon positions Object.keys(self.weapons).forEach(function (key) { // Standard positioning self.weapons[key].x = 10; self.weapons[key].y = 0; if (key === 'basic') { self.weapons[key].scale.x = 1.7; self.weapons[key].scale.y = 1.7; } else if (key === 'sniper') { self.weapons[key].scale.x = 1.2; self.weapons[key].scale.y = 1.2; } else { self.weapons[key].scale.x = 1.7; self.weapons[key].scale.y = 1.7; } }); // Create muzzle flash effects for each weapon self.muzzleFlashes = {}; Object.keys(self.weapons).forEach(function (key) { var flash = new MuzzleFlash(); self.weapons[key].addChild(flash); // Position at the end of each weapon flash.x = self.weapons[key].width - 10; flash.y = 0; if (key === 'super') { flash.graphics.tint = 0xFF00FF; // Magenta flash for super sniper } self.muzzleFlashes[key] = flash; }); self.fireRate = 1000; // ms between shots self.lastShot = 0; self.bulletDamage = 1; self.bulletSpeed = 15; // Default bullet speed self.currentWeapon = 'basic'; // Track current weapon type // Method to update rifle rotation based on cursor position self.updateAim = function (targetX, targetY) { // Calculate angle to target var angle = Math.atan2(targetY - self.y, targetX - self.x); // Determine if target is on left or right side of the screen var isTargetOnLeftSide = targetX < 2048 / 2; // Update sniper graphics based on which side target is on if (isTargetOnLeftSide) { // Target is on left side - character faces left self.graphics.scale.x = -1; // Flip character horizontally // Adjust weapon positions for left-facing stance Object.keys(self.weapons).forEach(function (key) { self.weapons[key].x = -10; // Offset for left side self.weapons[key].scale.x = -1; // Flip weapon horizontally }); } else { // Target is on right side - character faces right self.graphics.scale.x = 1; // Normal orientation // Adjust weapon positions for right-facing stance Object.keys(self.weapons).forEach(function (key) { self.weapons[key].x = 10; // Normal position on right side self.weapons[key].scale.x = 1; // Normal weapon orientation }); } // Set all weapons rotation to aim at target, properly adjusted for direction if (isTargetOnLeftSide) { // When facing left, we need to adjust the angle self.weapons.basic.rotation = angle + Math.PI; self.weapons.sniper.rotation = angle + Math.PI; self.weapons["super"].rotation = angle + Math.PI; } else { self.weapons.basic.rotation = angle; self.weapons.sniper.rotation = angle; self.weapons["super"].rotation = angle; } return angle; }; self.canShoot = function () { var now = Date.now(); if (now - self.lastShot >= self.fireRate) { self.lastShot = now; return true; } return false; }; self.switchWeapon = function (weaponIndex) { // Hide all weapons first self.weapons.basic.visible = false; self.weapons.sniper.visible = false; self.weapons["super"].visible = false; // Show selected weapon based on index switch (weaponIndex) { case 0: self.weapons.basic.visible = true; self.currentWeapon = 'basic'; break; case 1: self.weapons.sniper.visible = true; self.currentWeapon = 'sniper'; break; case 2: self.weapons["super"].visible = true; self.currentWeapon = 'super'; break; default: self.weapons.basic.visible = true; self.currentWeapon = 'basic'; } // Reset magazine if switching weapon var mag = magazines[self.currentWeapon]; if (mag && mag.current > mag.max) mag.current = mag.max; updateAmmoUI(); }; self.shoot = function (targetX, targetY) { // Magazine and reload logic var weapon = self.currentWeapon; var mag = magazines[weapon]; if (mag.reloading) return null; if (mag.current <= 0) { // Start reload mag.reloading = true; updateAmmoUI(); // Animate reload: move weapon down and up, and show "reloading..." in UI tween(self.weapons[weapon], { y: 60, alpha: 0.5 }, { duration: Math.floor(mag.reloadTime * 0.4), easing: tween.easeIn, onFinish: function onFinish() { tween(self.weapons[weapon], { y: 0, alpha: 1 }, { duration: Math.floor(mag.reloadTime * 0.6), easing: tween.easeOut }); } }); LK.setTimeout(function () { mag.current = mag.max; mag.reloading = false; updateAmmoUI(); }, mag.reloadTime); return null; } if (!self.canShoot()) return null; mag.current--; updateAmmoUI(); // Update rifle aim var angle = self.updateAim(targetX, targetY); var bullet = new Bullet(); // Determine if target is on left or right side of the screen var isTargetOnLeftSide = targetX < 2048 / 2; var activeWeapon = self.weapons[self.currentWeapon]; var rifleLength = activeWeapon.width; // Calculate bullet spawn position at the muzzle, always at the tip of the rifle in world space // Get the local muzzle position (rifle tip) in weapon's local space var muzzleLocalX = rifleLength; var muzzleLocalY = 0; // Transform muzzle position to world space // 1. Get weapon's rotation and scale.x (for left/right flip) var weaponRotation = activeWeapon.rotation; var weaponScaleX = activeWeapon.scale.x; // 2. Calculate rotated and flipped muzzle position var cosR = Math.cos(weaponRotation); var sinR = Math.sin(weaponRotation); var muzzleWorldX = self.x + (activeWeapon.x + muzzleLocalX * weaponScaleX) * cosR - muzzleLocalY * sinR; var muzzleWorldY = self.y + (activeWeapon.y + muzzleLocalX * weaponScaleX) * sinR + muzzleLocalY * cosR; // Place bullet at the muzzle tip bullet.x = muzzleWorldX; bullet.y = muzzleWorldY; // Set bullet direction - always use the original angle for bullet direction bullet.speedX = Math.cos(angle) * (self.currentWeapon === "super" ? self.bulletSpeed : bullet.speed); bullet.speedY = Math.sin(angle) * (self.currentWeapon === "super" ? self.bulletSpeed : bullet.speed); bullet.damage = self.bulletDamage; bullet.speed = self.bulletSpeed; // Set bullet speed from sniper // Customize bullet appearance based on weapon type if (self.currentWeapon === 'sniper') { bullet.graphics.tint = 0x33CCFF; // Blue tint for sniper bullets bullet.graphics.scale.set(0.8, 1.5); // Thinner, longer bullets bullet.weaponType = 'sniper'; } else if (self.currentWeapon === 'super') { bullet.graphics.tint = 0xFF00FF; // Magenta tint for super sniper bullets bullet.graphics.scale.set(1.2, 2.2); // Even longer, more powerful look bullet.damage = self.bulletDamage; // Ensure correct damage bullet.speed = self.bulletSpeed; // Ensure correct speed bullet.weaponType = 'super'; } else if (self.currentWeapon === 'tank') { bullet.graphics.tint = 0x00FF00; // Green for tank bullet.graphics.scale.set(1.5, 1.5); bullet.damage = self.bulletDamage; bullet.speed = self.bulletSpeed; bullet.weaponType = 'tank'; } else if (self.currentWeapon === 'fast') { bullet.graphics.tint = 0xFF00FF; // Magenta for fast bullet.graphics.scale.set(0.5, 0.7); bullet.damage = self.bulletDamage; bullet.speed = self.bulletSpeed; bullet.weaponType = 'fast'; } else if (self.currentWeapon === 'burn') { bullet.graphics.tint = 0xFF6600; bullet.graphics.scale.set(1.1, 1.1); bullet.weaponType = 'burn'; } else if (self.currentWeapon === 'freeze') { bullet.graphics.tint = 0x66CCFF; bullet.graphics.scale.set(1.1, 1.1); bullet.weaponType = 'freeze'; } else if (self.currentWeapon === 'poison') { bullet.graphics.tint = 0x00FF66; bullet.graphics.scale.set(1.1, 1.1); bullet.weaponType = 'poison'; } else if (self.currentWeapon === 'shieldPierce') { bullet.graphics.tint = 0xFFFF00; bullet.graphics.scale.set(1.1, 1.1); bullet.weaponType = 'shieldPierce'; } // Trigger muzzle flash for current weapon var muzzleFlash = self.muzzleFlashes[self.currentWeapon]; if (muzzleFlash) { muzzleFlash.flash(); } // Add weapon recoil effect based on weapon type var recoilDistance = 10; // Default recoil for rifle var recoilDuration = 100; // Default recoil recovery time var originX = activeWeapon.x; // Set different recoil parameters based on weapon type if (self.currentWeapon === 'sniper') { recoilDistance = 20; // Stronger recoil for sniper recoilDuration = 150; } else if (self.currentWeapon === 'super') { recoilDistance = 35; // Even stronger recoil for super sniper recoilDuration = 200; } // Calculate recoil direction (opposite to shot direction) // For recoil, we need to consider which direction the weapon is facing var recoilDirection = isTargetOnLeftSide ? 1 : -1; // Reverse for left-facing var bulletAngle = angle; // Use the same angle as bullet direction var recoilX = recoilDirection * Math.cos(bulletAngle) * recoilDistance; var recoilY = -Math.sin(bulletAngle) * recoilDistance; // Apply recoil to the active weapon tween(activeWeapon, { x: originX + recoilX, y: recoilY }, { duration: 50, // Quick recoil easing: tween.easeOut, onFinish: function onFinish() { // Recover from recoil tween(activeWeapon, { x: originX, y: 0 }, { duration: recoilDuration, easing: tween.easeInOut }); } }); LK.getSound('shoot').play(); return bullet; }; return self; }); var Tree = Container.expand(function () { var self = Container.call(this); // Random tree appearance with variation var type = Math.floor(Math.random() * 3); var size = Math.random() * 0.7 + 1.2; // Size between 1.2 and 1.9 var rotation = Math.random() * 0.2 - 0.1; // Slight rotation // Create the tree trunk using bullet shape with brown tint var treeTrunk = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, tint: 0x8B4513, // Saddle brown color scaleX: size * 0.3, scaleY: size * 0.8 }); // Create the tree crown using bullet shape with green tint var treeCrown = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, tint: 0x228B22, // Forest green color scaleX: size * 1.2, scaleY: size * 1.0 }); // Position crown on top of trunk treeCrown.y = -treeTrunk.height * 0.5; // Apply random rotation self.rotation = rotation; // Add some details to make trees look different from each other if (type === 0) { // First type - taller tree with darker green treeCrown.scale.y *= 1.4; treeCrown.tint = 0x006400; // Dark green } else if (type === 1) { // Second type - wider tree with lighter green treeCrown.scale.x *= 1.3; treeCrown.tint = 0x32CD32; // Lime green } else { // Third type - autumn tree with different color treeCrown.tint = 0xFF8C00; // Dark orange } return self; }); var Wall = Container.expand(function (type, health) { var self = Container.call(this); // Add shadow under wall self.shadow = new Shadow(); self.shadow.y = 60; self.addChildAt(self.shadow, 0); if (self.graphics) { var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1); if (self.shadow.updateSize) self.shadow.updateSize(baseWidth); self.shadow.x = 0; self.shadow.graphics.scale.x = baseWidth * 0.8 / 100; self.shadow.graphics.alpha = 0.5; } // Set properties based on wall type self.type = type || 'basic'; self.maxHealth = health || 100; self.health = self.maxHealth; // Create wall graphic based on type if (self.type === 'reinforced') { self.graphics = self.attachAsset('reinforcedWall', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } else if (self.type === 'sniper') { // Create a tower self.graphics = self.attachAsset('sniperTower', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); // Sniper tower stats self.maxHealth = 300; self.health = self.maxHealth; self.towerDamage = 2; self.towerRange = 700; self.towerFireRate = 1200; // ms between shots self.towerLastShot = 0; self.towerTarget = null; self.towerBulletSpeed = 18; self.towerBullet = function (target) { var bullet = new Bullet(); bullet.x = self.x; bullet.y = self.y - self.graphics.height / 2; var dx = target.x - bullet.x; var dy = target.y - bullet.y; var dist = Math.sqrt(dx * dx + dy * dy); // Prevent division by zero and ensure bullet always moves if (dist === 0) dist = 0.0001; bullet.speedX = dx / dist * self.towerBulletSpeed; bullet.speedY = dy / dist * self.towerBulletSpeed; bullet.damage = self.towerDamage; bullet.graphics.tint = 0x33CCFF; bullet.graphics.scale.set(0.7, 1.3); return bullet; }; // No need to add extra sniper on top since it's included in the image } else { // Basic wall self.graphics = self.attachAsset('basicWall', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); } // Add health bar self.healthBar = new Container(); self.addChild(self.healthBar); // Health bar background self.healthBarBg = LK.getAsset('bullet', { anchorX: 0.5, anchorY: 0.5, tint: 0x000000, scaleX: 3, scaleY: 0.3 }); self.healthBarBg.y = -40; self.healthBar.addChild(self.healthBarBg); // Health bar fill self.healthBarFill = LK.getAsset('bullet', { anchorX: 0, anchorY: 0.5, tint: 0x00FF00, scaleX: 3, scaleY: 0.2 }); self.healthBarFill.y = -40; self.healthBarFill.x = -self.healthBarBg.width / 2; self.healthBar.addChild(self.healthBarFill); // Hide health bar initially self.healthBar.visible = false; self.update = function () { // Show health bar if damaged, and always show for sniper tower if not full health if (self.health < self.maxHealth || self.type === 'sniper' && self.health < self.maxHealth) { self.healthBar.visible = true; // Update health bar fill var healthPercent = self.health / self.maxHealth; self.healthBarFill.scale.x = 3 * healthPercent; // Change color based on health if (healthPercent < 0.3) { self.healthBarFill.tint = 0xFF0000; // Red } else if (healthPercent < 0.6) { self.healthBarFill.tint = 0xFFFF00; // Yellow } else { self.healthBarFill.tint = 0x00FF00; // Green } } else { self.healthBar.visible = false; } // Sniper tower AI: shoot at nearest enemy in range if (self.type === 'sniper' && self.health > 0) { var now = Date.now(); // Find nearest enemy in range var nearest = null; var minDist = 99999; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (!e.active) continue; var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist && dist < self.towerRange) { minDist = dist; nearest = e; } } if (nearest && now - self.towerLastShot > self.towerFireRate) { self.towerLastShot = now; var bullet = self.towerBullet(nearest); if (bullet) { bullets.push(bullet); if (game && typeof game.addChild === "function") game.addChild(bullet); } } } }; self.takeDamage = function (amount) { self.health -= amount; // Show floating damage number on wall if (typeof showDamageNumber === "function") { showDamageNumber(self.x, self.y - 40, amount, 0xFFAA00); } if (self.health <= 0) { self.destroy(); return true; // Wall destroyed } return false; // Wall still standing }; return self; }); var WaveCountdown = Container.expand(function () { var self = Container.call(this); self.countdownTime = 60; // 60 seconds default self.active = false; // Create countdown text self.countdownText = new Text2(self.countdownTime, { size: 150, fill: 0xFFFFFF }); self.countdownText.anchor.set(0.5, 0.5); self.addChild(self.countdownText); // Create skip button self.skipButton = new Text2("SKIP", { size: 80, fill: 0xFFFF00 }); self.skipButton.anchor.set(0.5, 0.5); self.skipButton.y = 100; self.addChild(self.skipButton); // Skip button background for better visibility var skipButtonBg = LK.getAsset('bullet', { width: 200, height: 100, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); skipButtonBg.alpha = 0.8; skipButtonBg.y = 100; self.addChild(skipButtonBg); // Custom swap implementation since swapChildren is not available var parent = self.skipButton.parent; var skipButtonIndex = parent.children.indexOf(skipButtonBg); var buttonIndex = parent.children.indexOf(self.skipButton); // Manual reordering of children to create swap effect if (skipButtonIndex !== -1 && buttonIndex !== -1) { // Remove both from parent parent.removeChild(skipButtonBg); parent.removeChild(self.skipButton); // Add them back in reverse order parent.addChild(self.skipButton); parent.addChild(skipButtonBg); } // Timer for countdown self.timer = null; // Start countdown self.startCountdown = function (onComplete) { self.active = true; self.visible = true; self.countdownTime = 60; self.countdownText.setText(self.countdownTime); self.onComplete = onComplete; // Update countdown every second self.timer = LK.setInterval(function () { self.countdownTime--; self.countdownText.setText(self.countdownTime); // Update next wave timer UI if available if (typeof nextWaveTimerTxt !== "undefined") { nextWaveTimerTxt.setText('Next Wave: ' + self.countdownTime + 's'); } if (self.countdownTime <= 0) { self.stopCountdown(); if (self.onComplete) self.onComplete(); } }, 1000); }; // Stop countdown self.stopCountdown = function () { if (self.timer) { LK.clearInterval(self.timer); self.timer = null; } self.active = false; self.visible = false; }; // Skip button event handler self.skipButton.down = function (x, y, obj) { if (self.active) { self.stopCountdown(); if (self.onComplete) self.onComplete(); } }; return self; }); var WeaponShop = Container.expand(function () { var self = Container.call(this); self.isOpen = false; self.weapons = [{ name: "Basic Rifle", price: 0, damage: 1, speed: 15, owned: true, weaponType: "basic" }, { name: "Sniper Rifle", price: 50, damage: 3, speed: 30, owned: false, weaponType: "sniper" }, { name: "Super Sniper", price: 150, damage: 7, speed: 40, owned: false, weaponType: "super" }, { name: "Tank Gun", price: 90, damage: 5, speed: 18, owned: false, weaponType: "tank" }, { name: "Fast Blaster", price: 70, damage: 2, speed: 35, owned: false, weaponType: "fast" }, { name: "Burn Gun", price: 120, damage: 2, speed: 20, owned: false, weaponType: "burn" }, { name: "Freeze Gun", price: 120, damage: 2, speed: 20, owned: false, weaponType: "freeze" }, { name: "Poison Gun", price: 120, damage: 2, speed: 20, owned: false, weaponType: "poison" }, { name: "Shield Piercer", price: 200, damage: 3, speed: 25, owned: false, weaponType: "shieldPierce" }]; // Shop button self.shopButton = new Text2("WEAPONS", { size: 80, fill: 0xFFFF00 }); self.shopButton.anchor.set(0.5, 0); // Add background to make button more visible var buttonBg = LK.getAsset('bullet', { width: 300, height: 100, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); buttonBg.alpha = 0.8; buttonBg.y = self.shopButton.height / 2; self.addChild(buttonBg); self.addChild(self.shopButton); // Shop panel (hidden by default) self.panel = new Container(); self.panel.visible = false; self.addChild(self.panel); // Create weapon options self.weaponItems = []; for (var i = 0; i < self.weapons.length; i++) { var weapon = self.weapons[i]; var item = new Container(); var bg = LK.getAsset('bullet', { width: 400, height: 150, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); bg.alpha = 0.8; item.addChild(bg); var title = new Text2(weapon.name, { size: 40, fill: 0xFFFFFF }); title.anchor.set(0.5, 0); title.y = -50; item.addChild(title); var info = new Text2("Damage: " + weapon.damage + " | Speed: " + weapon.speed, { size: 30, fill: 0xFFFFFF }); info.anchor.set(0.5, 0); info.y = 0; item.addChild(info); var priceText = new Text2(weapon.owned ? "OWNED" : "$" + weapon.price, { size: 35, fill: weapon.owned ? 0x00FF00 : 0xFFFF00 }); priceText.anchor.set(0.5, 0); priceText.y = 40; item.addChild(priceText); item.y = i * 200; item.weaponIndex = i; self.weaponItems.push(item); self.panel.addChild(item); } // Position panel self.panel.y = 250; // Handle shop button press self.shopButton.down = function (x, y, obj) { self.toggleShop(); }; // Toggle shop visibility self.toggleShop = function () { self.isOpen = !self.isOpen; self.panel.visible = self.isOpen; }; // Handle weapon selection self.selectWeapon = function (index) { var weapon = self.weapons[index]; if (weapon.owned) { // Equip weapon sniper.bulletDamage = weapon.damage; sniper.bulletSpeed = weapon.speed; sniper.currentWeapon = weapon.weaponType || "basic"; sniper.switchWeapon(index); // Switch to selected weapon appearance // Visual feedback for equipped weapon for (var i = 0; i < self.weaponItems.length; i++) { var priceText = self.weaponItems[i].children[3]; if (i === index) { priceText.setText("EQUIPPED", { fill: i === 2 ? 0xFF00FF : 0x00FFFF // Magenta for super sniper, cyan for others }); } else if (self.weapons[i].owned) { priceText.setText("OWNED", { fill: i === 2 ? 0xFF00FF : 0x00FF00 // Magenta for super sniper, green for others }); } } LK.getSound('upgrade').play(); return true; } else if (currency >= weapon.price) { // Purchase weapon currency -= weapon.price; weapon.owned = true; // Update UI var priceText = self.weaponItems[index].children[3]; priceText.setText("EQUIPPED", { fill: index === 2 ? 0xFF00FF : 0x00FFFF // Magenta for super sniper, cyan for others }); // Reset other weapon texts to "OWNED" for (var i = 0; i < self.weaponItems.length; i++) { if (i !== index && self.weapons[i].owned) { self.weaponItems[i].children[3].setText("OWNED", { fill: i === 2 ? 0xFF00FF : 0x00FF00 // Magenta for super sniper, green for others }); } } // Equip weapon sniper.bulletDamage = weapon.damage; sniper.bulletSpeed = weapon.speed; sniper.switchWeapon(index); // Switch to selected weapon appearance LK.getSound('upgrade').play(); updateUI(); return true; } else { // Not enough currency LK.effects.flashScreen(0xFF0000, 300); return false; } }; // Check if an item was clicked self.checkItemClick = function (x, y) { if (!self.isOpen) return false; var pos = self.toLocal({ x: x, y: y }); for (var i = 0; i < self.weaponItems.length; i++) { var item = self.weaponItems[i]; // Use item's actual bounds for hit detection var bounds = item.children[0].getBounds(); // children[0] is the background // Convert bounds to local coordinates relative to the panel var itemLeft = item.x + bounds.x - bounds.width * item.children[0].anchorX; var itemRight = itemLeft + bounds.width; var itemTop = item.y + bounds.y - bounds.height * item.children[0].anchorY; var itemBottom = itemTop + bounds.height; if (pos.x >= itemLeft && pos.x <= itemRight && pos.y >= itemTop && pos.y <= itemBottom) { return self.selectWeapon(item.weaponIndex); } } return false; }; // Add down event handlers to each weapon item for (var i = 0; i < self.weaponItems.length; i++) { var item = self.weaponItems[i]; item.interactive = true; item.index = i; // Store the index // Using custom event handler for each item (function (index) { item.down = function (x, y, obj) { self.selectWeapon(index); }; })(i); } return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ // No title, no description // Always backgroundColor is black backgroundColor: 0x000000 }); /**** * Game Code ****/ // Dedicated shadow asset for under characters // Armored Vehicle (Zırhlı Araç) enemy assets for all directions // Create background var background = game.attachAsset('background', { width: 2048, height: 2732, anchorX: 0, anchorY: 0, tint: 0xC2B280 // Apply dry land tint to the background }); // Game variables var bunkerHealth = 100; var maxBunkerHealth = 100; var bunkerY = 2732 - 100; // Position near bottom of screen var currency = 0; var difficulty = 1; var wave = 1; var enemySpawnRate = 3000; // ms between enemy spawns var lastEnemySpawn = 0; var gameActive = true; var waveInProgress = false; var enemyIncreasePerWave = 0.25; // 25% more enemies per wave var enemiesPerWave = 25; // Starting with 25 enemies var enemiesSpawned = 0; // Track enemies spawned in current wave var enemiesRequired = 10; // Initial enemies required for first wave // Start score at 0 LK.setScore(0); // Magazine system for each weapon type var magazines = { basic: { max: 15, current: 15, reloading: false, reloadTime: 1200 }, sniper: { max: 10, current: 10, reloading: false, reloadTime: 1800 }, "super": { max: 5, current: 5, reloading: false, reloadTime: 2500 } }; // UI for ammo display var ammoTxt = new Text2('Ammo: 5/5', { size: 50, fill: 0xFFFF00 }); ammoTxt.anchor.set(1, 0); ammoTxt.x = 2048 - 80; ammoTxt.y = 60; LK.gui.top.addChild(ammoTxt); // Helper to update ammo UI function updateAmmoUI() { var weapon = sniper.currentWeapon; var mag = magazines[weapon]; if (mag.reloading) { ammoTxt.setText('Reloading...'); } else { ammoTxt.setText('Ammo: ' + mag.current + '/' + mag.max); } } // --- Floating damage numbers --- // Show floating text at (x, y) with value and color, and special effect for status function showDamageNumber(x, y, value, color, status) { var textValue = value; var textColor = color || 0xFF2222; var textSize = 48; var extraTween = null; var statusEffect = status || ""; // Status effect: show text and color for effect if (statusEffect === "CRIT") { textValue = "CRIT " + value; textColor = 0xFFD700; textSize = 60; } else if (statusEffect === "SLOW") { textValue = "SLOW"; textColor = 0x00FFFF; textSize = 44; } else if (statusEffect === "BURN") { textValue = "BURN"; textColor = 0xFF6600; textSize = 44; } else if (statusEffect === "FREEZE") { textValue = "FREEZE"; textColor = 0x66CCFF; textSize = 44; } else if (statusEffect === "POISON") { textValue = "POISON"; textColor = 0x00FF66; textSize = 44; } else if (statusEffect === "SHIELD") { textValue = "SHIELD"; textColor = 0xFFFF00; textSize = 44; } else if (statusEffect === "SHIELD BREAK") { textValue = "SHIELD BREAK"; textColor = 0xFFFF00; textSize = 44; } var dmgTxt = new Text2('-' + textValue, { size: textSize, fill: textColor }); dmgTxt.anchor.set(0.5, 0.5); dmgTxt.x = x; dmgTxt.y = y; dmgTxt.alpha = 1; game.addChild(dmgTxt); // Animate up and fade out, with a little scale pop for crit/status var scaleFrom = statusEffect === "CRIT" ? 1.5 : 1.1; dmgTxt.scale.set(scaleFrom, scaleFrom); tween(dmgTxt.scale, { x: 1, y: 1 }, { duration: 200, easing: tween.easeOut }); tween(dmgTxt, { y: y - 60, alpha: 0 }, { duration: 700, easing: tween.easeOut, onFinish: function onFinish() { game.removeChild(dmgTxt); } }); } // Upgrade costs and values var upgrades = { fireRate: { level: 1, cost: 10, value: 1000, // ms between shots increment: -100 // decrease time between shots }, bulletDamage: { level: 1, cost: 15, value: 1, increment: 1 // increase damage } }; // Arrays for tracking game objects var bullets = []; var enemies = []; var walls = []; // Ally instance (semi-transparent melee ally) var ally = null; // Create bunker var bunker = new Bunker(); bunker.x = 2048 / 2; bunker.y = bunkerY; game.addChild(bunker); // Create sniper var sniper = new Sniper(); sniper.x = 2048 / 2; sniper.y = bunkerY - 50; game.addChild(sniper); // Create weapon shop var weaponShop = new WeaponShop(); weaponShop.x = 350; // Moved more to the right for better visibility weaponShop.y = 1800; // Position higher on the screen for better visibility game.addChild(weaponShop); // Add to game instead of GUI for better positioning // Create building shop var buildingShop = new BuildingShop(); buildingShop.x = 400; // Moved further right to improve visibility and hit detection buildingShop.y = 600; // Repositioned lower on the screen for better accessibility game.addChild(buildingShop); // Create wave countdown timer var waveCountdown = new WaveCountdown(); waveCountdown.x = 2048 / 2; // Center horizontally waveCountdown.y = 2732 / 2; // Center vertically waveCountdown.visible = false; // Hide initially game.addChild(waveCountdown); // UI Elements // Score display var scoreTxt = new Text2('Score: 0', { size: 70, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); scoreTxt.y = 50; LK.gui.top.addChild(scoreTxt); // --- Mini-map UI --- // Mini-map config var minimapWidth = 320; var minimapHeight = 420; var minimapScaleX = minimapWidth / 2048; var minimapScaleY = minimapHeight / 2732; var minimapMargin = 40; // Mini-map container var minimapContainer = new Container(); minimapContainer.x = 2048 - minimapWidth - minimapMargin; minimapContainer.y = 2732 - minimapHeight - minimapMargin; minimapContainer.alpha = 0.85; // Mini-map background var minimapBg = LK.getAsset('bullet', { width: minimapWidth, height: minimapHeight, anchorX: 0, anchorY: 0, tint: 0x222222 }); minimapBg.alpha = 0.7; minimapContainer.addChild(minimapBg); // Mini-map bunker marker var minimapBunker = LK.getAsset('bullet', { width: 32, height: 32, anchorX: 0.5, anchorY: 0.5, tint: 0x00FF00 }); minimapBunker.alpha = 0.9; minimapContainer.addChild(minimapBunker); // Mini-map enemy markers (will be updated every frame) var minimapEnemyMarkers = []; // Add to game (not GUI, so it scales with world) game.addChild(minimapContainer); // Mini-map update function function updateMinimap() { // Update bunker marker minimapBunker.x = bunker.x * minimapScaleX; minimapBunker.y = bunker.y * minimapScaleY; // Remove old enemy markers for (var i = 0; i < minimapEnemyMarkers.length; i++) { minimapContainer.removeChild(minimapEnemyMarkers[i]); } minimapEnemyMarkers = []; // Add enemy markers for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (!e.active) continue; var marker = LK.getAsset('bullet', { width: 18, height: 18, anchorX: 0.5, anchorY: 0.5, tint: e.type === "armoredVehicle" ? 0xFF8800 : e.type === "tank" ? 0xFF0000 : 0xFFFF00 }); marker.x = e.x * minimapScaleX; marker.y = e.y * minimapScaleY; marker.alpha = 0.95; minimapContainer.addChild(marker); minimapEnemyMarkers.push(marker); } } // Super Ally button UI (above bomb button) var superAllyBtnBg = LK.getAsset('bullet', { width: 260, height: 110, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); superAllyBtnBg.alpha = 0.8; var superAllyBtn = new Text2("SUPER ALLY\n$1000", { size: 48, fill: 0xFF00FF }); superAllyBtn.anchor.set(0.5, 0.5); var superAllyBtnContainer = new Container(); superAllyBtnContainer.addChild(superAllyBtnBg); superAllyBtnContainer.addChild(superAllyBtn); superAllyBtnContainer.x = 140; superAllyBtnContainer.y = 2732 - 410; // Above bomb button superAllyBtnContainer.interactive = true; superAllyBtnContainer.visible = true; game.addChild(superAllyBtnContainer); // Bomb button UI (above ally button) var bombBtnBg = LK.getAsset('bullet', { width: 260, height: 110, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); bombBtnBg.alpha = 0.8; var bombBtn = new Text2("BOMB\n$50", { size: 55, fill: 0xFFD700 }); bombBtn.anchor.set(0.5, 0.5); var bombBtnContainer = new Container(); bombBtnContainer.addChild(bombBtnBg); bombBtnContainer.addChild(bombBtn); bombBtnContainer.x = 140; bombBtnContainer.y = 2732 - 280; // Above ally button bombBtnContainer.interactive = true; bombBtnContainer.visible = true; game.addChild(bombBtnContainer); // Super Ally effect state var superAllyActive = false; var superAllyCooldown = false; // Bomb state var bombMode = false; var bombBtnCooldown = false; // Super Ally button event superAllyBtnContainer.down = function (x, y, obj) { if (superAllyCooldown) return; if (currency >= 1000 && !superAllyActive) { currency -= 1000; updateUI(); superAllyActive = true; superAllyBtnBg.alpha = 0.4; superAllyBtn.alpha = 0.5; // Spawn 10 armored vehicles from back (top) edge, move to front (bottom), destroy all enemies in their path var superAllyVehicles = []; var startX = sniper.x - 220; // Spawn behind sniper horizontally var startY = 0 - 120; // Start above the visible area var endY = bunkerY - 120; // End just before the bunker var spacing = (2048 - 400) / 9; // Spread across width, avoid edges for (var i = 0; i < 10; i++) { var av = new Enemy("armoredVehicle"); av.hp = 99999; // Invincible av.damage = 9999; av.speed = 7; // Slowed down from 32 to 7 for more reasonable speed av.points = 0; av.currency = 0; av.type = "armoredVehicle"; // Use 'front' asset (facing down/forward) if (av.graphics) av.removeChild(av.graphics); av.graphics = av.attachAsset('armoredVehicle_front', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7 }); // Add shadow if (av.shadow) av.removeChild(av.shadow); av.shadow = new Shadow(); av.shadow.y = 40; var baseWidth = av.graphics.width * av.graphics.scale.x; av.shadow.updateSize(baseWidth); av.addChildAt(av.shadow, 0); av.shadow.x = 0; av.shadow.graphics.scale.x = baseWidth * 0.8 / 100; av.shadow.graphics.alpha = 0.5; // Spread vehicles horizontally, all start at the back (top) av.x = 200 + i * spacing; av.y = startY; av.superAlly = true; // Mark as super ally // --- Smoke effect state for this vehicle --- av.smokeParticles = []; av.smokeTick = 0; // --- Super Ally update with smoke effect --- av.update = function () { // Move forward (down) this.y += this.speed; // --- Smoke effect: spawn smoke every few frames behind the vehicle --- if (typeof this.smokeTick !== "number") this.smokeTick = 0; this.smokeTick++; if (this.smokeTick % 3 === 0) { // Create a smoke particle (small gray ellipse) var smoke = LK.getAsset('bullet', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7 + Math.random() * 0.5, scaleY: 0.5 + Math.random() * 0.3, tint: 0x888888 }); smoke.x = this.x + (Math.random() - 0.5) * 30; smoke.y = this.y - 60 + (Math.random() - 0.5) * 10; smoke.alpha = 0.7 + Math.random() * 0.2; smoke.life = 0; smoke.maxLife = 24 + Math.floor(Math.random() * 10); // Add to game and to this vehicle's smokeParticles array game.addChildAt(smoke, 0); this.smokeParticles.push(smoke); } // Update and fade out smoke particles for (var s = this.smokeParticles.length - 1; s >= 0; s--) { var sp = this.smokeParticles[s]; sp.life++; sp.y += 1.2 + Math.random() * 0.5; // Drift down sp.x += (Math.random() - 0.5) * 1.2; // Slight horizontal drift sp.alpha *= 0.96; // Fade out sp.scale.x *= 1.01; sp.scale.y *= 1.01; if (sp.life > sp.maxLife || sp.alpha < 0.05) { game.removeChild(sp); this.smokeParticles.splice(s, 1); } } // Destroy all enemies in path for (var j = enemies.length - 1; j >= 0; j--) { var e = enemies[j]; if (e.active && Math.abs(e.x - this.x) < 120 && Math.abs(e.y - this.y) < 80) { e.die(); } } // Remove self if off screen (past bunker) if (this.y > bunkerY + 200) { // Remove all smoke particles for (var s = this.smokeParticles.length - 1; s >= 0; s--) { game.removeChild(this.smokeParticles[s]); } this.smokeParticles = []; this.active = false; if (game && typeof game.removeChild === "function") game.removeChild(this); } }; superAllyVehicles.push(av); game.addChild(av); } // Animate button cooldown superAllyCooldown = true; LK.setTimeout(function () { superAllyActive = false; superAllyBtnBg.alpha = 0.8; superAllyBtn.alpha = 1; superAllyCooldown = false; }, 4000); // Remove all super ally vehicles after 4 seconds LK.setTimeout(function () { for (var i = 0; i < superAllyVehicles.length; i++) { if (superAllyVehicles[i] && game && typeof game.removeChild === "function") { game.removeChild(superAllyVehicles[i]); } } }, 4200); } else if (currency < 1000) { LK.effects.flashObject(superAllyBtnContainer, 0xFF0000, 200); } }; // Bomb button event bombBtnContainer.down = function (x, y, obj) { if (bombBtnCooldown) return; if (currency >= 50 && !bombMode) { bombMode = true; bombBtnBg.alpha = 0.4; bombBtn.alpha = 0.5; } else if (bombMode) { // Already in bomb mode, ignore } else { // Not enough money LK.effects.flashObject(bombBtnContainer, 0xFF0000, 200); } }; // Ally purchase button (bottom left corner, visible) var allyBtnBg = LK.getAsset('bullet', { width: 260, height: 110, anchorX: 0.5, anchorY: 0.5, tint: 0x333333 }); allyBtnBg.alpha = 0.8; var allyBtn = new Text2("ALLY\n$100", { size: 55, fill: 0x00FFFF }); allyBtn.anchor.set(0.5, 0.5); var allyBtnContainer = new Container(); allyBtnContainer.addChild(allyBtnBg); allyBtnContainer.addChild(allyBtn); allyBtnContainer.x = 140; allyBtnContainer.y = 2732 - 140; allyBtnContainer.interactive = true; allyBtnContainer.visible = true; game.addChild(allyBtnContainer); // Ally button event allyBtnContainer.down = function (x, y, obj) { if (currency >= 100 && (!ally || !ally.active)) { currency -= 100; updateUI(); if (ally && !ally.active) { game.removeChild(ally); } ally = new Ally(); // Spawn behind the sniper (above, on y axis) ally.x = sniper.x; ally.y = sniper.y + 120; game.addChild(ally); // --- Fix: Reset all enemy walk animation scale to default when ally is created --- for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (e && e.graphics && (e.type === 'fast' || e.type === 'tank' || e.type === 'regular')) { e.graphics.scale.x = 2.5; e.graphics.scale.y = 2.5; } } LK.effects.flashObject(ally, 0x00FFFF, 400); } else if (currency < 100) { LK.effects.flashObject(allyBtnContainer, 0xFF0000, 200); } }; // Wave display var waveTxt = new Text2('Wave: 1', { size: 50, fill: 0xFFFFFF }); waveTxt.anchor.set(1, 0); LK.gui.topLeft.addChild(waveTxt); waveTxt.x = 150; // Move away from the top left corner // Health display var healthTxt = new Text2('Bunker: 100%', { size: 50, fill: 0xFFFFFF }); healthTxt.anchor.set(0.5, 0); healthTxt.y = 130; LK.gui.top.addChild(healthTxt); // Enemy counter UI var enemyCounterTxt = new Text2('Enemies: 0', { size: 50, fill: 0xFF4444 }); enemyCounterTxt.anchor.set(1, 0); enemyCounterTxt.x = 2048 - 80; enemyCounterTxt.y = 130; LK.gui.top.addChild(enemyCounterTxt); // Next wave timer UI var nextWaveTimerTxt = new Text2('', { size: 50, fill: 0x00FFFF }); nextWaveTimerTxt.anchor.set(1, 0); nextWaveTimerTxt.x = 2048 - 80; nextWaveTimerTxt.y = 200; LK.gui.top.addChild(nextWaveTimerTxt); // Currency display var currencyTxt = new Text2('$: 0', { size: 50, fill: 0x00FF00 }); currencyTxt.anchor.set(0, 0); LK.gui.topLeft.addChild(currencyTxt); currencyTxt.x = 150; // Move away from top left corner currencyTxt.y = 60; // Position below wave display // Upgrade buttons removed as requested // Update UI elements function updateUI() { // Animate score if changed if (typeof updateUI.lastScore === "undefined") updateUI.lastScore = 0; var currentScore = LK.getScore(); if (currentScore !== updateUI.lastScore) { scoreTxt.setText('Score: ' + currentScore); // Animate score text pop scoreTxt.scale.set(1.2, 1.2); tween(scoreTxt.scale, { x: 1, y: 1 }, { duration: 200, easing: tween.easeOut }); updateUI.lastScore = currentScore; } // Animate currency if changed if (typeof updateUI.lastCurrency === "undefined") updateUI.lastCurrency = 0; if (currency !== updateUI.lastCurrency) { currencyTxt.setText('$: ' + currency); currencyTxt.scale.set(1.2, 1.2); tween(currencyTxt.scale, { x: 1, y: 1 }, { duration: 200, easing: tween.easeOut }); updateUI.lastCurrency = currency; } // Animate wave if changed if (typeof updateUI.lastWave === "undefined") updateUI.lastWave = 0; if (wave !== updateUI.lastWave) { waveTxt.setText('Wave: ' + wave); waveTxt.scale.set(1.2, 1.2); tween(waveTxt.scale, { x: 1, y: 1 }, { duration: 200, easing: tween.easeOut }); updateUI.lastWave = wave; } // Update enemy counter var aliveEnemies = 0; for (var i = 0; i < enemies.length; i++) { if (enemies[i] && enemies[i].active) aliveEnemies++; } enemyCounterTxt.setText('Enemies: ' + aliveEnemies + '/' + enemiesRequired); // Animate enemy counter if changed if (typeof updateUI.lastEnemies === "undefined") updateUI.lastEnemies = 0; if (aliveEnemies !== updateUI.lastEnemies) { enemyCounterTxt.scale.set(1.2, 1.2); tween(enemyCounterTxt.scale, { x: 1, y: 1 }, { duration: 200, easing: tween.easeOut }); updateUI.lastEnemies = aliveEnemies; } // Update next wave timer (if countdown active) if (waveCountdown && waveCountdown.active) { nextWaveTimerTxt.setText('Next Wave: ' + waveCountdown.countdownTime + 's'); // Animate timer if changed if (typeof updateUI.lastWaveTimer === "undefined") updateUI.lastWaveTimer = 0; if (waveCountdown.countdownTime !== updateUI.lastWaveTimer) { nextWaveTimerTxt.scale.set(1.2, 1.2); tween(nextWaveTimerTxt.scale, { x: 1, y: 1 }, { duration: 200, easing: tween.easeOut }); updateUI.lastWaveTimer = waveCountdown.countdownTime; } } else { nextWaveTimerTxt.setText(''); } updateUpgradeButtons(); } function updateBunkerHealth() { var healthPercentage = Math.max(0, Math.min(100, Math.round(bunkerHealth / maxBunkerHealth * 100))); healthTxt.setText('Bunker: ' + healthPercentage + '%'); // Animate health text color based on health if (healthPercentage < 30) { healthTxt.setStyle({ fill: 0xFF2222 }); } else if (healthPercentage < 60) { healthTxt.setStyle({ fill: 0xFFFF00 }); } else { healthTxt.setStyle({ fill: 0xFFFFFF }); } // Animate health text pop if health drops if (typeof updateBunkerHealth.lastHealth === "undefined") updateBunkerHealth.lastHealth = 100; if (healthPercentage < updateBunkerHealth.lastHealth) { healthTxt.scale.set(1.2, 1.2); tween(healthTxt.scale, { x: 1, y: 1 }, { duration: 200, easing: tween.easeOut }); } updateBunkerHealth.lastHealth = healthPercentage; bunker.showDamage(bunkerHealth / maxBunkerHealth); // Only make damage indicator visible and shake when actually taking damage if (bunkerHealth < maxBunkerHealth) { bunker.damageIndicator.alpha = 1; tween(bunker.damageIndicator, { x: bunker.damageIndicator.originalX + (Math.random() * 10 - 5), y: bunker.damageIndicator.originalY + (Math.random() * 10 - 5) }, { duration: 50, repeat: 5, yoyo: true, onFinish: function onFinish() { LK.setTimeout(function () { if (gameActive) { tween(bunker.damageIndicator, { alpha: 0 }, { duration: 300 }); } }, 1000); } }); } // Create a visual indicator effect when health is low if (bunkerHealth / maxBunkerHealth < 0.5) { LK.setTimeout(function () { if (gameActive) bunker.showDamage(bunkerHealth / maxBunkerHealth); }, 100); } } function updateUpgradeButtons() { // Upgrade buttons removed as requested } // Game mechanics functions // Track if armored vehicle has spawned this game if (typeof armoredVehicleSpawned === "undefined") { var armoredVehicleSpawned = false; } if (typeof armoredVehicleWave === "undefined") { var armoredVehicleWave = 0; // Track the last wave armored vehicle spawned } function spawnEnemy() { var now = Date.now(); if (!waveInProgress || now - lastEnemySpawn < enemySpawnRate) return; // Check if we've already spawned enough enemies for this wave if (enemiesSpawned >= enemiesRequired) return; // Limit: never spawn more than 10 alive enemies at once var aliveEnemies = 0; for (var i = 0; i < enemies.length; i++) { if (enemies[i] && enemies[i].active) aliveEnemies++; } if (aliveEnemies >= 10) return; lastEnemySpawn = now; // Calculate number of enemies to spawn based on score var currentScore = LK.getScore(); var enemiesToSpawnAtOnce = 1; // Default spawn one enemy at a time // Increase enemies spawned at once based on more gradual score thresholds if (currentScore >= 1200) { enemiesToSpawnAtOnce = 5; // 5 at 1200+ } else if (currentScore >= 900) { enemiesToSpawnAtOnce = 4; // 4 at 900+ } else if (currentScore >= 500) { enemiesToSpawnAtOnce = 3; // 3 at 500+ } else if (currentScore >= 200) { enemiesToSpawnAtOnce = 2; // 2 at 200+ } // Make sure we don't spawn more enemies than required for this wave enemiesToSpawnAtOnce = Math.min(enemiesToSpawnAtOnce, enemiesRequired - enemiesSpawned); // Also, do not spawn more than (10 - aliveEnemies) at once enemiesToSpawnAtOnce = Math.min(enemiesToSpawnAtOnce, 10 - aliveEnemies); // If no room to spawn, return if (enemiesToSpawnAtOnce <= 0) return; // Spawn armored vehicle after 800 score, once per wave if (currentScore >= 800 && waveInProgress && wave > 0 && armoredVehicleWave !== wave && aliveEnemies < 10) { var armoredVehicle = new Enemy("armoredVehicle"); armoredVehicle.hp = 100; armoredVehicle.damage = 15; // Zırhlı araç hasarı 15 olarak ayarlandı armoredVehicle.speed = 3.5; armoredVehicle.points = 100; armoredVehicle.currency = 20; // Use a random direction asset for variety var directions = ["armoredVehicle_front", "armoredVehicle_back", "armoredVehicle_left", "armoredVehicle_right", "armoredVehicle_frontLeft", "armoredVehicle_frontRight", "armoredVehicle_backLeft", "armoredVehicle_backRight"]; var dirAsset = directions[Math.floor(Math.random() * directions.length)]; if (armoredVehicle.graphics) armoredVehicle.removeChild(armoredVehicle.graphics); armoredVehicle.graphics = armoredVehicle.attachAsset(dirAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7 }); // Add shadow if (armoredVehicle.shadow) armoredVehicle.removeChild(armoredVehicle.shadow); armoredVehicle.shadow = new Shadow(); armoredVehicle.shadow.y = 40; var baseWidth = armoredVehicle.graphics.width * armoredVehicle.graphics.scale.x; armoredVehicle.shadow.updateSize(baseWidth); armoredVehicle.addChildAt(armoredVehicle.shadow, 0); armoredVehicle.shadow.x = 0; armoredVehicle.shadow.graphics.scale.x = baseWidth * 0.8 / 100; armoredVehicle.shadow.graphics.alpha = 0.5; armoredVehicle.type = "armoredVehicle"; armoredVehicle.x = Math.random() * (2048 - 200) + 100; armoredVehicle.y = -150; enemies.push(armoredVehicle); game.addChild(armoredVehicle); enemiesSpawned++; armoredVehicleWave = wave; // Do not spawn other enemies this tick if armored vehicle is spawned return; } // Spawn multiple enemies at once for (var i = 0; i < enemiesToSpawnAtOnce; i++) { // Determine enemy type based on score and wave progression var enemyType = 'regular'; var random = Math.random(); // More advanced enemies appear based on score thresholds (slower, more gradual progression) if (currentScore >= 700 && random < 0.13) { enemyType = 'tank'; } else if (currentScore >= 500 && random < 0.11) { enemyType = 'tank'; } else if (currentScore >= 300 && random < 0.09) { enemyType = 'tank'; } else if (currentScore >= 200 && random < 0.18) { enemyType = 'fast'; } else if (currentScore >= 100 && random < 0.10) { enemyType = 'fast'; } var enemy; if (enemyType === 'armoredVehicle') { // Should not happen here, armored vehicle is handled above continue; } else { enemy = new Enemy(enemyType); // Randomly assign advanced status effects for variety if (Math.random() < 0.08) { enemy.hasShield = true; } if (Math.random() < 0.05) { enemy.burningUntil = Date.now() + 2000 + Math.random() * 2000; enemy.burnTick = Date.now(); } if (Math.random() < 0.05) { enemy.poisonedUntil = Date.now() + 2000 + Math.random() * 2000; enemy.poisonTick = Date.now(); } if (Math.random() < 0.04) { enemy.slowedUntil = Date.now() + 2000 + Math.random() * 2000; enemy.slowFactor = 0.4; } } // Distribute enemies across the width of the screen if (enemiesToSpawnAtOnce > 1) { // Distribute evenly but with some randomness var segment = 2048 / enemiesToSpawnAtOnce; enemy.x = i * segment + Math.random() * (segment - 100) + 50; } else { enemy.x = Math.random() * (2048 - 100) + 50; // Random x position } if (enemyType === 'desertBandit') { enemy.y = -120; // Spawn desert bandit further back } else { enemy.y = -50; // Start above the screen } enemies.push(enemy); game.addChild(enemy); // Increment enemies spawned counter enemiesSpawned++; } } function checkCollisions() { for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; if (!bullet.active) { game.removeChild(bullet); bullets.splice(i, 1); continue; } for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (!enemy.active) { game.removeChild(enemy); enemies.splice(j, 1); continue; } // Allow bullets to damage all enemies, including DesertBandit if (bullet.active && enemy.active && bullet.intersects(enemy)) { // Determine weaponType and crit var weaponType = 'basic'; var isCritical = false; if (sniper && sniper.currentWeapon) { weaponType = sniper.currentWeapon; // 10% crit chance for sniper, 20% for super, 5% for rifle if (weaponType === 'sniper' && Math.random() < 0.10) isCritical = true; if (weaponType === 'super' && Math.random() < 0.20) isCritical = true; if (weaponType === 'basic' && Math.random() < 0.05) isCritical = true; } enemy.takeDamage(bullet.damage, weaponType, isCritical); bullet.hit(); break; } } } } function upgradeFireRate() { if (currency >= upgrades.fireRate.cost) { currency -= upgrades.fireRate.cost; upgrades.fireRate.level++; upgrades.fireRate.value += upgrades.fireRate.increment; // Ensure fire rate doesn't go below minimum upgrades.fireRate.value = Math.max(200, upgrades.fireRate.value); sniper.fireRate = upgrades.fireRate.value; // Increase cost for next upgrade upgrades.fireRate.cost = Math.floor(upgrades.fireRate.cost * 1.5); LK.getSound('upgrade').play(); updateUI(); } } function upgradeBulletDamage() { if (currency >= upgrades.bulletDamage.cost) { currency -= upgrades.bulletDamage.cost; upgrades.bulletDamage.level++; upgrades.bulletDamage.value += upgrades.bulletDamage.increment; sniper.bulletDamage = upgrades.bulletDamage.value; // Increase cost for next upgrade upgrades.bulletDamage.cost = Math.floor(upgrades.bulletDamage.cost * 1.5); LK.getSound('upgrade').play(); updateUI(); } } function increaseDifficulty() { // Check if all enemies for this wave are spawned and eliminated if (waveInProgress && enemiesSpawned >= enemiesRequired && enemies.length === 0) { // All enemies in current wave defeated, prepare for next wave wave++; // Calculate new enemies required for next wave (increase by 25%) enemiesRequired = Math.ceil(enemiesPerWave * Math.pow(1 + 0.25, wave - 1)); // Reset enemies spawned counter enemiesSpawned = 0; // Decrease spawn rate with each wave (faster spawns) based on score and wave var currentScore = LK.getScore(); // Calculate spawn rate based on score: higher score = faster spawn, but never below 2200ms // Example: every 80 score reduces spawn rate by 120ms, but never below 2200ms var scoreSpawnReduction = Math.floor(currentScore / 80) * 120; enemySpawnRate = Math.max(2200, 3200 - scoreSpawnReduction); // Also apply wave-based reduction, but never below 2200ms, and reduce per wave by only 60ms enemySpawnRate = Math.max(2200, enemySpawnRate - (wave - 1) * 60); // Pause wave progression and show countdown waveInProgress = false; // Start countdown for next wave waveCountdown.startCountdown(function () { // When countdown completes, start the new wave startNewWave(); }); } } function startNewWave() { // Clear all existing enemies for (var i = enemies.length - 1; i >= 0; i--) { game.removeChild(enemies[i]); } enemies = []; // Reset enemies spawned counter enemiesSpawned = 0; // Update UI to show new wave and enemy count waveTxt.setText('Wave: ' + wave + ' (' + enemiesRequired + ' enemies)'); // Start spawning enemies again waveInProgress = true; // Flash screen to indicate new wave LK.effects.flashScreen(0x00FF00, 500); // Adjust difficulty based on score var currentScore = LK.getScore(); if (currentScore > 300) { difficulty = 5; } else if (currentScore > 200) { difficulty = 4; } else if (currentScore > 100) { difficulty = 3; } else if (currentScore > 50) { difficulty = 2; } else { difficulty = 1; } } function gameOver() { gameActive = false; // Save high score if (LK.getScore() > storage.highScore) { storage.highScore = LK.getScore(); } // Save currency for next game storage.currency = currency; // Show game over screen LK.showGameOver(); } // Event handlers game.move = function (x, y, obj) { if (gameActive) { // If placing a wall, move it with cursor if (game.isPlacing && game.placeableWall) { game.placeableWall.x = x; game.placeableWall.y = y; } else { // Otherwise update rifle aim to follow cursor var angle = sniper.updateAim(x, y); // Let the updateAim method handle the character orientation // We don't rotate the sniper character itself anymore // This prevents the character from being upside down when aiming } } }; game.down = function (x, y, obj) { if (!gameActive) return; // Check if weapon shop item was clicked if (weaponShop.checkItemClick(x, y)) { return; } // Check if building shop item was clicked if (buildingShop.checkItemClick(x, y)) { return; } // If placing a wall, place it at the clicked position if (game.isPlacing && game.placeableWall) { // Don't place near bunker or too close to top-left corner var distToBunker = Math.sqrt(Math.pow(x - bunker.x, 2) + Math.pow(y - bunker.y, 2)); var distToTopLeft = Math.sqrt(Math.pow(x - 100, 2) + Math.pow(y - 100, 2)); if (distToBunker < 150 || distToTopLeft < 100 || y > bunkerY - 50) { // Can't place here - flash red LK.effects.flashObject(game.placeableWall, 0xFF0000, 300); return; } // Create an actual wall at this position var wall = new Wall(game.placeableWall.buildingType, game.placeableWall.buildingHealth); wall.x = x; wall.y = y; // --- Begin: Overlap and stacking logic for buildings --- // If placing a sniper tower, check for overlap with other walls and adjust stacking order if (wall.type === "sniper" && game.walls && game.walls.length > 0) { // Find all overlapping walls (excluding self) var overlapping = []; for (var i = 0; i < game.walls.length; i++) { var other = game.walls[i]; if (!other || !other.graphics) continue; // Simple bounding box overlap check var dx = Math.abs(wall.x - other.x); var dy = Math.abs(wall.y - other.y); var combinedHalfWidth = (wall.graphics.width * wall.graphics.scale.x + other.graphics.width * other.graphics.scale.x) / 2; var combinedHalfHeight = (wall.graphics.height * wall.graphics.scale.y + other.graphics.height * other.graphics.scale.y) / 2; if (dx < combinedHalfWidth && dy < combinedHalfHeight) { overlapping.push(other); } } // If overlap, ensure sniper tower is above if closer to player (higher y) if (overlapping.length > 0) { // Find the wall with the highest y (closest to player) var topWall = wall; for (var i = 0; i < overlapping.length; i++) { if (overlapping[i].y > topWall.y) { topWall = overlapping[i]; } } // Add new wall to game game.addChild(wall); // If the new wall is the top wall, bring it to the top if (topWall === wall) { game.setChildIndex(wall, game.children.length - 1); } else { // Otherwise, ensure the top wall is above game.setChildIndex(topWall, game.children.length - 1); } } else { game.addChild(wall); } } else if (game.walls && game.walls.length > 0) { // If placing a non-sniper wall, check for overlap with sniper towers and other walls var overlapping = []; for (var i = 0; i < game.walls.length; i++) { var other = game.walls[i]; if (!other || !other.graphics) continue; var dx = Math.abs(wall.x - other.x); var dy = Math.abs(wall.y - other.y); var combinedHalfWidth = (wall.graphics.width * wall.graphics.scale.x + other.graphics.width * other.graphics.scale.x) / 2; var combinedHalfHeight = (wall.graphics.height * wall.graphics.scale.y + other.graphics.height * other.graphics.scale.y) / 2; if (dx < combinedHalfWidth && dy < combinedHalfHeight) { overlapping.push(other); } } if (overlapping.length > 0) { // Find the wall with the highest y (closest to player) var topWall = wall; for (var i = 0; i < overlapping.length; i++) { if (overlapping[i].y > topWall.y) { topWall = overlapping[i]; } } game.addChild(wall); if (topWall === wall) { game.setChildIndex(wall, game.children.length - 1); } else { game.setChildIndex(topWall, game.children.length - 1); } } else { game.addChild(wall); } } else { // No other walls, just add game.addChild(wall); } // --- End: Overlap and stacking logic for buildings --- // Add to walls array if we don't have one if (!game.walls) { game.walls = []; } game.walls.push(wall); // Remove placeable wall game.removeChild(game.placeableWall); game.placeableWall = null; game.isPlacing = false; return; } // Bomb placement logic if (typeof bombMode !== "undefined" && bombMode) { // Don't allow bomb in top left 100x100 or on UI if (x < 100 && y < 100) { LK.effects.flashObject(bombBtnContainer, 0xFF0000, 200); return; } // Place bomb at this location var bomb = new Bomb(); bomb.x = x; bomb.y = y - 400; // Drop from above game.addChild(bomb); // Deduct money currency -= 50; updateUI(); // Reset bomb button state bombMode = false; bombBtnBg.alpha = 0.8; bombBtn.alpha = 1; // Cooldown: prevent spamming bombBtnCooldown = true; LK.setTimeout(function () { bombBtnCooldown = false; }, 1200); return; } // Fire at touch location (single shot for all weapons) var bullet = sniper.shoot(x, y); if (bullet) { bullets.push(bullet); game.addChild(bullet); } }; game.up = function (x, y, obj) { // No machine gun auto fire to stop }; // Update function called every frame game.update = function () { if (!gameActive) return; // Only spawn enemies if a wave is in progress if (waveInProgress) { // Spawn enemies spawnEnemy(); // Update enemies for (var i = 0; i < enemies.length; i++) { if (enemies[i].active) { enemies[i].update(); } } // Check for collisions checkCollisions(); } // Update bullets regardless of wave status for (var i = 0; i < bullets.length; i++) { if (bullets[i].active) { bullets[i].update(); } } // Update walls and check for wall-enemy collisions if (game.walls && game.walls.length > 0) { for (var i = game.walls.length - 1; i >= 0; i--) { var wall = game.walls[i]; // Update wall health bar wall.update(); // Check for collisions with enemies for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (enemy.active && wall.intersects(enemy)) { // Stop enemy and set it to attack the wall if (!enemy.isAttackingWall) { enemy.isAttackingWall = true; enemy.attackTarget = wall; enemy.attackAnimationTicks = 0; } // Check if wall was destroyed after the attack if (wall.health <= 0) { // Remove wall if destroyed game.removeChild(wall); game.walls.splice(i, 1); // Reset any enemies attacking this wall for (var k = 0; k < enemies.length; k++) { if (enemies[k].attackTarget === wall) { enemies[k].isAttackingWall = false; enemies[k].attackTarget = null; // Reset enemy position and rotation tween(enemies[k].graphics, { rotation: 0, y: 0 }, { duration: 200, easing: tween.easeOut }); } } break; } } } } } // Update muzzle flashes Object.keys(sniper.muzzleFlashes).forEach(function (key) { if (sniper.muzzleFlashes[key].active) { sniper.muzzleFlashes[key].update(); } }); // Update bombs if (typeof game.children !== "undefined") { for (var i = game.children.length - 1; i >= 0; i--) { var child = game.children[i]; // Super Ally vehicles: update and remove if inactive if (child && child.superAlly && child.active && typeof child.update === "function") { child.update(); if (!child.active) { game.removeChild(child); } } // Bombs if (child && child instanceof Container && child.update && child instanceof Bomb && child.active) { child.update(); // Remove bomb if not visible anymore if (child.exploded && child.visible === false) { game.removeChild(child); } } } } // Update ally if present if (ally && ally.active) { ally.update(); } else if (ally && !ally.active) { // Remove dead ally from game game.removeChild(ally); ally = null; } // Update difficulty (handles wave transitions) increaseDifficulty(); // Continuously update damage indicator when health is low if (bunkerHealth / maxBunkerHealth < 0.4) { // Move damage indicator more frequently as health gets lower if (LK.ticks % Math.max(5, Math.floor(bunkerHealth / maxBunkerHealth * 20)) === 0) { bunker.showDamage(bunkerHealth / maxBunkerHealth); } } // Update UI if (LK.ticks % 30 === 0) { updateUI(); } // Update minimap every frame updateMinimap(); }; // Make sure the weapon shop starts with the appropriate weapon equipped weaponShop.selectWeapon(0); // Start with basic rifle selected updateAmmoUI(); // Show initial ammo // Initialize UI updateUI(); // Ensure damage indicator is invisible at start bunker.damageIndicator.alpha = 0; updateBunkerHealth(); // Calculate initial enemies required for first wave enemiesRequired = enemiesPerWave; // Start the first wave with countdown waveInProgress = false; waveCountdown.startCountdown(function () { startNewWave(); }); // Place random bushes and trees around the game area function placeBushes() { // Number of decorative elements to place var bushCount = 25; var treeCount = 15; // Positions to avoid (bunker and sniper area) var avoidX = 2048 / 2; var avoidY = bunkerY; var avoidRadius = 200; // Also avoid the top-left corner where menu icon is located var topLeftX = 50; var topLeftY = 50; var topLeftRadius = 100; // Add bushes for (var i = 0; i < bushCount; i++) { var bush = new Bush(); // Keep generating positions until we find a suitable one var validPosition = false; var attempts = 0; while (!validPosition && attempts < 10) { // Generate random position bush.x = Math.random() * 2048; bush.y = Math.random() * 2732; // Check distance from bunker area var distToBunker = Math.sqrt(Math.pow(bush.x - avoidX, 2) + Math.pow(bush.y - avoidY, 2)); // Check distance from top-left corner var distToTopLeft = Math.sqrt(Math.pow(bush.x - topLeftX, 2) + Math.pow(bush.y - topLeftY, 2)); // Position is valid if it's away from both areas to avoid if (distToBunker > avoidRadius && distToTopLeft > topLeftRadius) { validPosition = true; } attempts++; } // Add bush behind other game elements (insert at the beginning of children array) game.addChildAt(bush, 0); } // Add trees for (var i = 0; i < treeCount; i++) { var tree = new Tree(); // Keep generating positions until we find a suitable one var validPosition = false; var attempts = 0; while (!validPosition && attempts < 10) { // Generate random position tree.x = Math.random() * 2048; tree.y = Math.random() * 2732; // Check distance from bunker area var distToBunker = Math.sqrt(Math.pow(tree.x - avoidX, 2) + Math.pow(tree.y - avoidY, 2)); // Check distance from top-left corner var distToTopLeft = Math.sqrt(Math.pow(tree.x - topLeftX, 2) + Math.pow(tree.y - topLeftY, 2)); // Position is valid if it's away from both areas to avoid if (distToBunker > avoidRadius && distToTopLeft > topLeftRadius) { validPosition = true; } attempts++; } // Add tree behind other game elements (insert at the beginning of children array) game.addChildAt(tree, 0); } } // Add bushes to the game placeBushes(); // Start background music LK.playMusic('gameBgMusic', { fade: { start: 0, end: 0.3, duration: 1000 } }); // Armored vehicle asset naming legend: // front: ileri (aşağı) // back: geri (yukarı) // left: sola // right: sağa // frontLeft: çapraz aşağı-sola // frontRight: çapraz aşağı-sağa // backLeft: çapraz yukarı-sola // backRight: çapraz yukarı-sağa
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0,
currency: 0
});
/****
* Classes
****/
// Ally: Semi-transparent melee ally that follows sniper and attacks nearest enemy
var Ally = Container.expand(function () {
var self = Container.call(this);
// Use a unique ally soldier image as the ally's body
self.graphics = self.attachAsset('allySoldier', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
self.graphics.alpha = 0.85; // Slightly transparent for distinction
// Add shadow under ally (after graphics is created)
self.shadow = new Shadow();
self.shadow.y = 48;
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.alpha = 0.5;
self.addChildAt(self.shadow, 0);
// In update, keep shadow under the character
var _superUpdate = self.update;
self.update = function () {
// Shadow follows character
if (self.shadow) {
self.shadow.x = 0;
self.shadow.y = 40;
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
}
if (_superUpdate) _superUpdate.apply(self, arguments);
};
self.maxHealth = 100;
self.health = self.maxHealth;
self.damage = 1;
self.speed = 3.2; // Faster than regular enemy speed
self.attackRange = 90; // Melee range
self.attackCooldown = 600; // ms between attacks
// Use regular enemy walk animation for Ally
self.walkAnimTick = 0;
self.lastMoveDirection = 1;
self.updateWalkAnim = function () {
// Diagonal walk animation: rotate left and right while moving
if (self.active) {
// Determine direction (toward target or following sniper)
var dir = 1;
if (self.targetEnemy) {
dir = self.targetEnemy.x > self.x ? 1 : -1;
} else if (sniper) {
dir = sniper.x > self.x ? 1 : -1;
}
self.lastMoveDirection = dir;
// Diagonal walk: oscillate rotation left and right as walking
// Use a sine wave for smooth left-right rotation
var walkOsc = Math.sin(LK.ticks * 0.18) * 0.28; // amplitude controls max angle (radians)
tween(self.graphics, {
rotation: walkOsc
}, {
duration: 180,
easing: tween.easeInOut
});
}
// Add up/down bobbing for walk animation (like enemy)
var walkCycle = Math.sin(LK.ticks * 0.15) * 6;
self.graphics.y = walkCycle;
};
self.lastAttack = 0;
self.targetEnemy = null;
self.followDistance = 80; // Distance to keep from sniper
self.active = true;
// Health bar background
self.healthBarBg = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x000000,
scaleX: 1.6,
scaleY: 0.22
});
self.healthBarBg.y = 60;
self.addChild(self.healthBarBg);
// Health bar fill
self.healthBar = LK.getAsset('bullet', {
anchorX: 0,
anchorY: 0.5,
tint: 0x00FF00,
scaleX: 1.5,
scaleY: 0.18
});
self.healthBar.y = 60;
self.healthBar.x = -self.healthBarBg.width / 2;
self.addChild(self.healthBar);
// Update health bar
self.updateHealthBar = function () {
var percent = Math.max(0, self.health / self.maxHealth);
self.healthBar.scale.x = 1.5 * percent;
if (percent < 0.3) {
self.healthBar.tint = 0xFF0000;
} else if (percent < 0.6) {
self.healthBar.tint = 0xFFFF00;
} else {
self.healthBar.tint = 0x00FF00;
}
self.healthBar.visible = percent < 1;
self.healthBarBg.visible = percent < 1;
};
// Take damage
self.takeDamage = function (amount) {
self.health -= amount;
self.updateHealthBar();
LK.effects.flashObject(self, 0xff0000, 200);
// Show floating damage number
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 40, amount, 0x00FFFF);
}
if (self.health <= 0) {
self.active = false;
tween(self, {
alpha: 0
}, {
duration: 400
});
self.visible = false;
}
};
// Ally update: follow sniper or attack nearest enemy
self.update = function () {
if (!self.active) return;
// Prevent ally from dying if it moves off the top or sides of the screen
if (self.x < 50) self.x = 50;
if (self.x > 2048 - 50) self.x = 2048 - 50;
if (self.y < -100) self.y = -100;
self.updateHealthBar();
self.updateWalkAnim();
// Find nearest enemy anywhere on the map (no range limit)
var nearest = null;
var minDist = 99999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (!e.active) continue;
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearest = e;
}
}
var now = Date.now();
if (nearest) {
// Always move toward nearest enemy if any exist
self.targetEnemy = nearest;
var angle = Math.atan2(nearest.y - self.y, nearest.x - self.x);
// If not in attack range, move toward enemy
if (minDist > self.attackRange * 0.7) {
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
// Attack if in range and cooldown ready
if (minDist <= self.attackRange && now - self.lastAttack > self.attackCooldown) {
self.lastAttack = now;
// 10% chance to stun, 10% chance to slow, 5% chance to confuse, 10% crit
var isCrit = Math.random() < 0.10;
var effectRoll = Math.random();
if (isCrit) {
nearest.takeDamage(self.damage * 2, 'basic', true);
} else if (effectRoll < 0.10) {
nearest.takeDamage(self.damage, 'stun');
} else if (effectRoll < 0.20) {
nearest.takeDamage(self.damage, 'slow');
} else if (effectRoll < 0.25) {
nearest.takeDamage(self.damage, 'confuse');
} else {
nearest.takeDamage(self.damage, 'basic');
}
LK.effects.flashObject(nearest, 0x00FFFF, 120);
}
} else {
// No enemy present, follow sniper
self.targetEnemy = null;
var dx = sniper.x - self.x;
var dy = sniper.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > self.followDistance) {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed * 0.7;
self.y += Math.sin(angle) * self.speed * 0.7;
}
}
};
return self;
});
// Bomb: Air-dropped bomb that bounces and explodes
var Bomb = Container.expand(function () {
var self = Container.call(this);
// Bomb body (yellow ellipse)
self.bombBody = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 2.2,
tint: 0xFFD700 // Gold/yellow
});
// Shadow
self.shadow = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 0.5,
tint: 0x000000
});
self.shadow.alpha = 0.35;
self.shadow.y = self.bombBody.height * 1.1;
self.addChild(self.shadow);
self.vy = 0; // vertical speed
self.gravity = 2.2; // gravity
self.bounceCount = 0;
self.maxBounces = 1; // Only bounce once
self.bounced = false;
self.exploded = false;
self.groundY = null; // set on spawn
// Explosion effect
self.explode = function () {
if (self.exploded) return;
self.exploded = true;
// Flash and scale up bomb
tween(self.bombBody, {
scaleX: 5,
scaleY: 5,
alpha: 0
}, {
duration: 220,
easing: tween.easeOut,
onFinish: function onFinish() {
self.visible = false;
self.active = false;
}
});
// Flash screen
LK.effects.flashScreen(0xFFFF00, 200);
// Damage all enemies in radius
var radius = 220;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (!e.active) continue;
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < radius) {
e.takeDamage(20, 'basic', true);
}
}
// Show explosion text
showDamageNumber(self.x, self.y - 60, "BOOM", 0xFFD700, "CRIT");
};
self.update = function () {
if (self.exploded) return;
if (self.groundY === null) {
// Set groundY to the first y where bomb lands (simulate ground)
self.groundY = self.y + 320;
}
// Move bomb down
self.y += self.vy;
self.vy += self.gravity;
// Shadow follows x, stays at groundY
self.shadow.x = 0;
self.shadow.y = self.bombBody.height * 1.1 + (self.groundY - self.y);
// Bounce logic
if (!self.bounced && self.y >= self.groundY) {
self.bounced = true;
self.bounceCount++;
// Bounce up with less speed
self.y = self.groundY;
self.vy = -22;
// Squash bomb for bounce
tween(self.bombBody, {
scaleY: 1.2,
scaleX: 2.8
}, {
duration: 80,
yoyo: true,
repeat: 1
});
// Play bounce sound/effect (optional)
} else if (self.bounced && self.y >= self.groundY) {
// Landed after bounce, explode
self.y = self.groundY;
self.vy = 0;
self.explode();
}
};
self.active = true;
return self;
});
var BuildingShop = Container.expand(function () {
var self = Container.call(this);
self.isOpen = false;
self.buildings = [{
name: "Basic Wall",
price: 10,
health: 100,
owned: false
}, {
name: "Reinforced Wall",
price: 20,
health: 200,
owned: false
}, {
name: "Sniper Tower",
price: 100,
health: 300,
damage: 2,
fireRate: 2000,
owned: false
}];
// Shop button
self.shopButton = new Text2("BUILDINGS", {
size: 80,
fill: 0xFFFF00
});
self.shopButton.anchor.set(0.5, 0);
// Add background to make button more visible
var buttonBg = LK.getAsset('bullet', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
buttonBg.alpha = 0.8;
buttonBg.y = self.shopButton.height / 2;
self.addChild(buttonBg);
self.addChild(self.shopButton);
// Shop panel (hidden by default)
self.panel = new Container();
self.panel.visible = false;
self.addChild(self.panel);
// Create building options
self.buildingItems = [];
for (var i = 0; i < self.buildings.length; i++) {
var building = self.buildings[i];
var item = new Container();
var bg = LK.getAsset('bullet', {
width: 400,
height: 150,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
bg.alpha = 0.8;
item.addChild(bg);
var title = new Text2(building.name, {
size: 40,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0);
title.y = -50;
item.addChild(title);
var info;
if (building.name === "Sniper Tower") {
info = new Text2("Health: " + building.health + " | Damage: " + building.damage, {
size: 30,
fill: 0xFFFFFF
});
} else {
info = new Text2("Health: " + building.health, {
size: 30,
fill: 0xFFFFFF
});
}
info.anchor.set(0.5, 0);
info.y = 0;
item.addChild(info);
var priceText = new Text2(building.owned ? "OWNED" : "$" + building.price, {
size: 35,
fill: building.owned ? 0x00FF00 : 0xFFFF00
});
priceText.anchor.set(0.5, 0);
priceText.y = 40;
item.addChild(priceText);
item.y = i * 200;
item.buildingIndex = i;
self.buildingItems.push(item);
self.panel.addChild(item);
}
// Position panel
self.panel.y = 250;
// Handle shop button press
self.shopButton.down = function (x, y, obj) {
self.toggleShop();
};
// Toggle shop visibility
self.toggleShop = function () {
self.isOpen = !self.isOpen;
self.panel.visible = self.isOpen;
};
// Handle building selection/purchase
self.selectBuilding = function (index) {
var building = self.buildings[index];
if (currency >= building.price) {
// Always allow purchase, deduct currency every time
currency -= building.price;
updateUI();
// Update UI to show price (never "OWNED")
var priceText = self.buildingItems[index].children[3];
priceText.setText("$" + building.price, {
fill: 0xFFFF00
});
LK.getSound('upgrade').play();
// After purchase, immediately select it for placement
var buildingType;
var buildingHealth = building.health;
if (index === 0) {
buildingType = 'basic';
} else if (index === 1) {
buildingType = 'reinforced';
} else if (index === 2) {
buildingType = 'sniper';
}
// Create and add placeable wall
game.placeableWall = new PlaceableWall(buildingType, buildingHealth);
game.addChild(game.placeableWall);
game.isPlacing = true;
// Close shop
self.toggleShop();
return true;
} else if (building.owned) {
// (Legacy: If owned, allow placement without payment, but this should never be true now)
var buildingType;
var buildingHealth = building.health;
if (index === 0) {
buildingType = 'basic';
} else if (index === 1) {
buildingType = 'reinforced';
} else if (index === 2) {
buildingType = 'sniper';
}
game.placeableWall = new PlaceableWall(buildingType, buildingHealth);
game.addChild(game.placeableWall);
game.isPlacing = true;
self.toggleShop();
return true;
} else {
// Not enough currency
LK.effects.flashScreen(0xFF0000, 300);
return false;
}
};
// Check if an item was clicked
self.checkItemClick = function (x, y) {
if (!self.isOpen) return false;
var pos = self.toLocal({
x: x,
y: y
});
for (var i = 0; i < self.buildingItems.length; i++) {
var item = self.buildingItems[i];
// Use item's actual bounds for hit detection
var bounds = item.children[0].getBounds(); // children[0] is the background
// Convert bounds to local coordinates relative to the panel
var itemLeft = item.x + bounds.x - bounds.width * item.children[0].anchorX;
var itemRight = itemLeft + bounds.width;
var itemTop = item.y + bounds.y - bounds.height * item.children[0].anchorY;
var itemBottom = itemTop + bounds.height;
if (pos.x >= itemLeft && pos.x <= itemRight && pos.y >= itemTop && pos.y <= itemBottom) {
return self.selectBuilding(item.buildingIndex);
}
}
return false;
};
// Add down event handlers to each building item
for (var i = 0; i < self.buildingItems.length; i++) {
var item = self.buildingItems[i];
item.interactive = true;
item.index = i; // Store the index
// Using custom event handler for each item
(function (index) {
item.down = function (x, y, obj) {
self.selectBuilding(index);
};
})(i);
}
return self;
});
var Bullet = Container.expand(function () {
var self = Container.call(this);
// No shadow for bullets
self.graphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15; // Default speed, will be overridden
self.damage = 1; // Default damage, will be overridden
self.active = true;
self.update = function () {
if (!self.active) return;
// Use directional movement if speedX and speedY are defined
if (self.speedX !== undefined && self.speedY !== undefined) {
self.x += self.speedX;
self.y += self.speedY;
} else {
// Fallback to original vertical-only movement
self.y -= self.speed;
}
// Bullet goes off screen (check all edges)
if (self.y < -50 || self.y > 2732 + 50 || self.x < -50 || self.x > 2048 + 50) {
self.active = false;
}
};
self.hit = function () {
self.active = false;
LK.getSound('enemyHit').play();
LK.effects.flashObject(self, 0xffffff, 200);
};
return self;
});
var Bunker = Container.expand(function () {
var self = Container.call(this);
// Add shadow under bunker
self.shadow = new Shadow();
self.shadow.y = 60;
self.addChildAt(self.shadow, 0);
if (self.graphics) {
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.scale.x = baseWidth * 0.8 / 100;
self.shadow.graphics.alpha = 0.5;
}
self.graphics = self.attachAsset('bunker', {
anchorX: 0.5,
anchorY: 0.5
});
self.damageIndicator = self.attachAsset('damageIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
self.damageIndicator.alpha = 0;
self.damageIndicator.originalX = 0;
self.damageIndicator.originalY = 0;
self.showDamage = function (percentage) {
// Make indicator more visible as health decreases
self.damageIndicator.alpha = 1 - percentage;
// Move the indicator based on health percentage
// Lower health = more movement
var moveFactor = (1 - percentage) * 10;
self.damageIndicator.x = self.damageIndicator.originalX + (Math.random() * 2 - 1) * moveFactor;
self.damageIndicator.y = self.damageIndicator.originalY + (Math.random() * 2 - 1) * moveFactor;
// Change tint to become redder as health decreases
var healthColor = Math.floor(percentage * 255);
self.damageIndicator.tint = 255 << 16 | healthColor << 8 | healthColor;
};
// Store original position
self.onAddedToStage = function () {
self.damageIndicator.originalX = self.damageIndicator.x;
self.damageIndicator.originalY = self.damageIndicator.y;
};
return self;
});
var Bush = Container.expand(function () {
var self = Container.call(this);
// Random bush appearance with slight variation
var type = Math.floor(Math.random() * 3);
var size = Math.random() * 0.5 + 0.8; // Size between 0.8 and 1.3
var rotation = Math.random() * 0.3 - 0.15; // Slight rotation
// Create the bush using bullet shape with green tint
self.graphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x2E8B57,
// Sea green color
scaleX: size,
scaleY: size
});
// Apply random rotation
self.graphics.rotation = rotation;
// Add some details to make bushes look different from each other
if (type === 0) {
// First type - taller bush
self.graphics.scale.y *= 1.3;
self.graphics.tint = 0x228B22; // Forest green
} else if (type === 1) {
// Second type - wider bush
self.graphics.scale.x *= 1.2;
self.graphics.tint = 0x006400; // Dark green
} else {
// Third type - round bush
self.graphics.tint = 0x3CB371; // Medium sea green
}
return self;
});
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'regular';
// Set properties based on enemy type
switch (self.type) {
case 'fast':
self.graphics = self.attachAsset('fastEnemy', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
self.speed = 3;
self.hp = 1;
self.damage = 10;
self.points = 15;
self.currency = 2;
break;
case 'tank':
self.graphics = self.attachAsset('tankEnemy', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
self.speed = 1;
self.hp = 5;
self.damage = 25;
self.points = 30;
self.currency = 5;
break;
default:
// regular
self.graphics = self.attachAsset('regularEnemy', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
self.speed = 2;
self.hp = 2;
self.damage = 15;
self.points = 10;
self.currency = 1;
}
// Create and add shadow beneath enemy (after graphics is created)
self.shadow = new Shadow();
// Position shadow further in front of enemy, and a bit more forward
self.shadow.y = self.graphics.height * 0.62; // More in front
var baseWidth = self.graphics.width * self.graphics.scale.x;
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.alpha = 0.5;
self.addChildAt(self.shadow, 0); // Add shadow behind the enemy
// In update, keep shadow under the character
var _superUpdate = self.update;
self.update = function () {
// Shadow always follows enemy, not walk animation
if (self.shadow) {
self.shadow.x = 0;
self.shadow.y = self.graphics.height * 0.62; // More in front
var baseWidth = self.graphics.width * self.graphics.scale.x;
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
}
if (_superUpdate) _superUpdate.apply(self, arguments);
};
self.active = true;
self.update = function () {
if (!self.active) return;
// --- Begin: Status effect processing ---
// Burning: take 1 damage every 400ms while burning
if (self.burningUntil && Date.now() < self.burningUntil) {
if (!self.burnTick) self.burnTick = Date.now();
if (Date.now() - self.burnTick > 400) {
self.burnTick = Date.now();
self.hp -= 1;
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 40, 1, 0xFF6600);
}
LK.effects.flashObject(self, 0xFF6600, 100);
if (self.hp <= 0) {
self.die();
return;
}
}
} else if (self.burningUntil && Date.now() >= self.burningUntil) {
self.burningUntil = null;
self.burnTick = null;
}
// Poison: take 1 damage every 600ms while poisoned
if (self.poisonedUntil && Date.now() < self.poisonedUntil) {
if (!self.poisonTick) self.poisonTick = Date.now();
if (Date.now() - self.poisonTick > 600) {
self.poisonTick = Date.now();
self.hp -= 1;
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 40, 1, 0x00FF66);
}
LK.effects.flashObject(self, 0x00FF66, 100);
if (self.hp <= 0) {
self.die();
return;
}
}
} else if (self.poisonedUntil && Date.now() >= self.poisonedUntil) {
self.poisonedUntil = null;
self.poisonTick = null;
}
// Shield: show visual effect if shielded
if (self.hasShield && self.graphics) {
if (!self.shieldGlow) {
self.shieldGlow = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.8,
scaleY: 2.8,
tint: 0xFFFF00
});
self.shieldGlow.alpha = 0.25;
self.addChildAt(self.shieldGlow, 1);
}
} else if (self.shieldGlow) {
self.removeChild(self.shieldGlow);
self.shieldGlow = null;
}
// --- End: Status effect processing ---
// --- Status effect handling: stun, slow, confuse ---
var now = Date.now();
if (self.stunnedUntil && now < self.stunnedUntil) {
// Stunned: skip all movement and attack, but allow animation
if (self.graphics) {
tween(self.graphics, {
rotation: 0
}, {
duration: 120
});
}
// Still allow walk animation for visual feedback
var walkCycle = Math.sin(LK.ticks * 0.15) * 6;
self.graphics.y = walkCycle;
return;
} else if (self.stunnedUntil && now >= self.stunnedUntil) {
self.stunnedUntil = null;
}
if (self.slowedUntil && now < self.slowedUntil) {
self.speedModifier = self.slowFactor || 0.5;
} else if (self.slowedUntil && now >= self.slowedUntil) {
self.slowedUntil = null;
self.speedModifier = 1;
}
if (self.confusedUntil && now < self.confusedUntil) {
// Randomize move direction
if (Math.random() < 0.1) {
self.moveDirection = Math.random() > 0.5 ? 1 : -1;
}
} else if (self.confusedUntil && now >= self.confusedUntil) {
self.confusedUntil = null;
}
// Default speed modifier
if (typeof self.speedModifier !== "number") self.speedModifier = 1;
// Armored vehicle animation: switch asset for all 8 directions (including diagonals)
if (self.type === "armoredVehicle" && self.graphics) {
// Prevent asset switch if _preventAssetSwitch is set (e.g. during damage flash)
if (self._preventAssetSwitch) {
// Still update lastX/lastY for correct direction after flash
self.lastX = self.x;
self.lastY = self.y;
} else {
// Calculate movement direction
var dx = 0,
dy = 0;
if (typeof self.lastX === "number" && typeof self.lastY === "number") {
dx = self.x - self.lastX;
dy = self.y - self.lastY;
}
// Only update if moved enough to change direction
// Only switch asset if direction meaningfully changed (not for tiny dx/dy)
var minMoveDist = 2.5; // Only consider direction change if moved at least this much
if (Math.abs(dx) > minMoveDist || Math.abs(dy) > minMoveDist) {
var dirAsset = "armoredVehicle_front";
// Calculate angle in degrees for direction
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
// Normalize angle to [0, 360)
if (angle < 0) angle += 360;
// 8-directional asset selection
if (angle >= 337.5 || angle < 22.5) {
dirAsset = "armoredVehicle_right";
} else if (angle >= 22.5 && angle < 67.5) {
dirAsset = "armoredVehicle_frontRight";
} else if (angle >= 67.5 && angle < 112.5) {
dirAsset = "armoredVehicle_front";
} else if (angle >= 112.5 && angle < 157.5) {
dirAsset = "armoredVehicle_frontLeft";
} else if (angle >= 157.5 && angle < 202.5) {
dirAsset = "armoredVehicle_left";
} else if (angle >= 202.5 && angle < 247.5) {
dirAsset = "armoredVehicle_backLeft";
} else if (angle >= 247.5 && angle < 292.5) {
dirAsset = "armoredVehicle_back";
} else if (angle >= 292.5 && angle < 337.5) {
dirAsset = "armoredVehicle_backRight";
}
// Always use 'front' asset if moving almost straight down (dy > 0, dx is small)
if (Math.abs(dx) < minMoveDist && dy > minMoveDist) {
dirAsset = "armoredVehicle_front";
}
// Only change asset if needed (and only if direction changed)
if (!self.graphics.assetId || self.graphics.assetId !== dirAsset) {
// Remove old graphics
self.removeChild(self.graphics);
self.graphics = self.attachAsset(dirAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7
});
// Always set assetId to prevent asset loss
self.graphics.assetId = dirAsset;
// Ensure shadow stays behind
if (self.shadow) {
self.setChildIndex(self.shadow, 0);
}
}
// Save last direction asset for next frame
self._lastArmoredVehicleAsset = dirAsset;
}
// Save last position for next frame
self.lastX = self.x;
self.lastY = self.y;
}
}
// Basic diagonal movement pattern
if (!self.moveDirection) {
self.moveDirection = Math.random() > 0.5 ? 1 : -1; // Random initial direction
self.moveCounter = 0;
self.maxMoveDistance = Math.random() * 30 + 20; // Random move distance
self.isAttackingWall = false;
self.attackTarget = null;
self.attackAnimationTicks = 0;
}
// If enemy is attacking a wall or ally, perform attack animation
if (self.isAttackingWall && self.attackTarget) {
// --- Always run walk animation, even while attacking ---
// Diagonal walk: oscillate rotation left and right as walking (like Ally)
var walkOsc = Math.sin(LK.ticks * 0.18) * 0.28;
// Always reset scale to default before tween to prevent cumulative shrinking
self.graphics.scale.x = self.type === 'fast' || self.type === 'tank' || self.type === 'regular' ? 2.5 : self.graphics.scale.x;
self.graphics.scale.y = self.type === 'fast' || self.type === 'tank' || self.type === 'regular' ? 2.5 : self.graphics.scale.y;
tween(self.graphics, {
rotation: walkOsc
}, {
duration: 180,
easing: tween.easeInOut
});
// Add up/down bobbing for walk animation
var walkCycle = Math.sin(LK.ticks * 0.15) * 6;
self.graphics.y = walkCycle;
// --- Ava fix: If being tweened (knockback), do not freeze in place ---
// If a knockback tween is active, allow movement to resume after knockback ends
if (self._knockbackTweenActive) {
// Wait for knockback tween to finish before resuming attack animation
return;
}
// Increment animation counter
self.attackAnimationTicks++;
// Every 45 frames complete a full attack cycle
if (self.attackAnimationTicks < 20) {
// Pull back for attack - move head backwards
if (self.attackAnimationTicks === 1) {
tween(self.graphics, {
rotation: -0.3,
y: -10
}, {
duration: 300,
easing: tween.easeOut
});
}
} else if (self.attackAnimationTicks < 25) {
// Forward strike - move head forward quickly
if (self.attackAnimationTicks === 20) {
tween(self.graphics, {
rotation: 0.2,
y: 10
}, {
duration: 150,
easing: tween.easeIn
});
}
} else {
// Reset animation counter and damage the wall or ally
self.attackAnimationTicks = 0;
// Apply damage to wall or ally
if (self.attackTarget) {
if (self.attackTarget.takeDamage) {
self.attackTarget.takeDamage(self.damage / 5);
LK.effects.flashObject(self.attackTarget, 0xFF0000, 200);
}
}
}
// When wall or ally is destroyed, resume movement
if (!self.attackTarget || self.attackTarget.health !== undefined && self.attackTarget.health <= 0 || self.attackTarget.active !== undefined && self.attackTarget.active === false) {
self.isAttackingWall = false;
self.attackTarget = null;
// Reset position and rotation
tween(self.graphics, {
rotation: 0,
y: 0
}, {
duration: 200,
easing: tween.easeOut
});
}
return; // Don't move while attacking
}
// --- Begin: Sniper Tower targeting logic ---
// Priority: Attack sniper tower if present and in range, else ally, else bunker
var sniperTowerWall = null;
var minTowerDist = 99999;
if (typeof game !== "undefined" && game.walls && game.walls.length > 0) {
for (var i = 0; i < game.walls.length; i++) {
var wall = game.walls[i];
if (wall.type === "sniper" && wall.health > 0) {
var dxTower = wall.x - self.x;
var dyTower = wall.y - self.y;
var distTower = Math.sqrt(dxTower * dxTower + dyTower * dyTower);
if (distTower < minTowerDist) {
minTowerDist = distTower;
sniperTowerWall = wall;
}
}
}
}
if (sniperTowerWall) {
// Move toward sniper tower and attack if in range
var dxTower = sniperTowerWall.x - self.x;
var dyTower = sniperTowerWall.y - self.y;
var distTower = Math.sqrt(dxTower * dxTower + dyTower * dyTower);
// --- FIX: Armored vehicle must attack only from close range, not from distance ---
var attackRange = 60;
if (self.type === "armoredVehicle") {
attackRange = 60; // Same as other enemies, no ranged attack
}
if (distTower < attackRange) {
if (!self.isAttackingWall || self.attackTarget !== sniperTowerWall) {
self.isAttackingWall = true;
self.attackTarget = sniperTowerWall;
self.attackAnimationTicks = 0;
}
// Do not move if attacking tower
return;
} else {
// Move toward sniper tower if not in attack range
var angleToTower = Math.atan2(dyTower, dxTower);
self.x += Math.cos(angleToTower) * self.speed;
self.y += Math.sin(angleToTower) * self.speed;
// Add walking animation for following tower
if (LK.ticks % 10 === 0 && !self.isAttackingWall) {
tween(self.graphics, {
rotation: (dxTower > 0 ? 1 : -1) * 0.1
}, {
duration: 250,
easing: tween.easeInOut
});
}
return;
}
}
// --- End: Sniper Tower targeting logic ---
// Check for ally in range and attack if present and active
if (typeof ally !== "undefined" && ally && ally.active) {
var dxAlly = ally.x - self.x;
var dyAlly = ally.y - self.y;
var distAlly = Math.sqrt(dxAlly * dxAlly + dyAlly * dyAlly);
// If ally is present, always follow and attack ally
if (distAlly < 60) {
if (!self.isAttackingWall || self.attackTarget !== ally) {
self.isAttackingWall = true;
self.attackTarget = ally;
self.attackAnimationTicks = 0;
}
// Do not move if attacking ally
return;
} else {
// Move toward ally if not in attack range
var angleToAlly = Math.atan2(dyAlly, dxAlly);
self.x += Math.cos(angleToAlly) * self.speed;
self.y += Math.sin(angleToAlly) * self.speed;
// Add walking animation for following ally
if (LK.ticks % 10 === 0 && !self.isAttackingWall) {
tween(self.graphics, {
rotation: (dxAlly > 0 ? 1 : -1) * 0.1
}, {
duration: 250,
easing: tween.easeInOut
});
}
return;
}
}
// Move diagonally
self.y += self.speed;
self.x += self.moveDirection * (self.speed * 0.5);
// Switch direction after a certain distance
self.moveCounter += self.speed;
if (self.moveCounter >= self.maxMoveDistance) {
self.moveDirection *= -1; // Reverse direction
self.moveCounter = 0;
self.maxMoveDistance = Math.random() * 30 + 20; // New random distance
}
// Make the shadow grow or shrink slightly with height simulation
// Calculate height simulation based on movement cycle
var heightSimulation = Math.sin(LK.ticks * 0.05) * 0.1;
// Update shadow properties to create floating effect
self.shadow.graphics.alpha = 0.5 - heightSimulation * 0.1; // Vary opacity with more baseline visibility
self.shadow.graphics.scale.x = self.graphics.width * self.graphics.scale.x * 0.8 / 100 * (1 - heightSimulation); // Vary size
// Ensure shadow follows enemy even with movement
self.shadow.x = 0;
// Add walking animation (always run, not just every 10 ticks, to prevent animation freeze)
if (!self.isAttackingWall) {
// Only apply walk animation to non-armoredVehicle types
if (self.type === 'fast' || self.type === 'tank' || self.type === 'regular') {
// Diagonal walk: oscillate rotation left and right as walking (like Ally)
var walkOsc = Math.sin(LK.ticks * 0.18) * 0.28;
// Always reset scale to default before tween to prevent cumulative shrinking
self.graphics.scale.x = 2.5;
self.graphics.scale.y = 2.5;
tween(self.graphics, {
rotation: walkOsc
}, {
duration: 180,
easing: tween.easeInOut
});
// Add up/down bobbing for walk animation
var walkCycle = Math.sin(LK.ticks * 0.15) * 6;
self.graphics.y = walkCycle;
}
// For armoredVehicle, do not animate walk at all (no rotation, no bobbing)
}
// Ensure enemy stays within screen bounds (but do NOT kill enemy if off screen, just clamp position)
if (self.x < 50) {
self.x = 50;
self.moveDirection = 1;
} else if (self.x > 2048 - 50) {
self.x = 2048 - 50;
self.moveDirection = -1;
}
// Prevent enemy from dying if it moves off the top or sides of the screen
if (self.y < -100) {
self.y = -100;
}
// Check if enemy reached the bunker
if (self.y > bunkerY - 50) {
self.attackBunker();
}
};
self.takeDamage = function (amount, weaponType, isCritical) {
// --- Begin: Advanced status effects and reactions ---
weaponType = weaponType || 'basic';
isCritical = !!isCritical;
// Burning: continuous damage over time, red flash
if (weaponType === 'burn' && !self.burningUntil) {
self.burningUntil = Date.now() + 2500;
self.burnTick = Date.now();
LK.effects.flashObject(self, 0xFF6600, 200);
}
// Freezing: slow and blue flash
if (weaponType === 'freeze') {
self.slowedUntil = Date.now() + 2500;
self.slowFactor = 0.3;
LK.effects.flashObject(self, 0x66CCFF, 200);
}
// Poison: green flash, damage over time
if (weaponType === 'poison' && !self.poisonedUntil) {
self.poisonedUntil = Date.now() + 4000;
self.poisonTick = Date.now();
LK.effects.flashObject(self, 0x00FF66, 200);
}
// Shield: absorb one hit, yellow flash
if (self.hasShield) {
self.hasShield = false;
LK.effects.flashObject(self, 0xFFFF00, 400);
// Show floating shield break
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 60, "SHIELD", 0xFFFF00);
}
return;
}
// --- End: Advanced status effects and reactions ---
// Default weaponType to 'basic' if not provided
weaponType = weaponType || 'basic';
isCritical = !!isCritical;
// Critical hit: 2x damage, special effect
if (isCritical) {
amount = Math.floor(amount * 2);
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 60, amount, 0xFFD700); // Gold color for crit
}
LK.effects.flashObject(self, 0xFFD700, 350);
// Briefly scale up enemy for crit
tween(self.graphics, {
scaleX: self.type === 'armoredVehicle' ? 1.7 : self.graphics.scale.x * 1.3,
scaleY: self.type === 'armoredVehicle' ? 1.7 : self.graphics.scale.y * 1.3
}, {
duration: 80,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
if (self.type === 'fast' || self.type === 'tank' || self.type === 'regular') {
self.graphics.scale.x = 2.5;
self.graphics.scale.y = 2.5;
} else if (self.type === 'armoredVehicle') {
self.graphics.scale.x = 1.7;
self.graphics.scale.y = 1.7;
}
}
});
}
self.hp -= amount;
// Show floating damage number
if (!isCritical && typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 40, amount, 0xFF2222);
}
// --- Weapon-specific hit reactions ---
if (self.hp > 0) {
// Rifle: 10% chance to briefly stun
if (weaponType === 'basic' && Math.random() < 0.10) {
self.stunnedUntil = Date.now() + 350;
LK.effects.flashObject(self, 0x00FFFF, 120);
}
// Sniper: always knockback
if (weaponType === 'sniper') {
var angle = Math.atan2(self.y - sniper.y, self.x - sniper.x);
var knockbackDist = 60 + Math.random() * 30;
var targetX = self.x + Math.cos(angle) * knockbackDist;
var targetY = self.y + Math.sin(angle) * knockbackDist;
// Clamp to screen
targetX = Math.max(50, Math.min(2048 - 50, targetX));
targetY = Math.max(0, Math.min(2732 - 50, targetY));
// --- Ava fix: Set knockback tween flag so update() can allow movement after knockback ---
// Only apply knockback tween if not armoredVehicle, armoredVehicle just flashes
if (self.type === "armoredVehicle") {
// Only flash, do not hide or move, and do NOT remove/re-add graphics to prevent flicker
if (self.graphics) {
// Temporarily set a flag to prevent asset switching in update during flash
self._preventAssetSwitch = true;
LK.effects.flashObject(self, 0x33CCFF, 180);
// Remove the flag after flash duration
LK.setTimeout(function () {
self._preventAssetSwitch = false;
}, 180);
}
} else {
self._knockbackTweenActive = true;
tween(self, {
x: targetX,
y: targetY
}, {
duration: 180,
easing: tween.easeOut,
onFinish: function onFinish() {
self._knockbackTweenActive = false;
}
});
// Briefly flash blue
LK.effects.flashObject(self, 0x33CCFF, 180);
}
}
// Super Sniper: stun and strong knockback
if (weaponType === 'super') {
var angle = Math.atan2(self.y - sniper.y, self.x - sniper.x);
var knockbackDist = 120 + Math.random() * 40;
var targetX = self.x + Math.cos(angle) * knockbackDist;
var targetY = self.y + Math.sin(angle) * knockbackDist;
targetX = Math.max(50, Math.min(2048 - 50, targetX));
targetY = Math.max(0, Math.min(2732 - 50, targetY));
self._knockbackTweenActive = true;
tween(self, {
x: targetX,
y: targetY
}, {
duration: 220,
easing: tween.easeOut,
onFinish: function onFinish() {
self._knockbackTweenActive = false;
}
});
self.stunnedUntil = Date.now() + 600;
LK.effects.flashObject(self, 0xFF00FF, 220);
}
// Tank: 20% chance to slow enemy for 2 seconds
if (weaponType === 'tank' && Math.random() < 0.20) {
self.slowedUntil = Date.now() + 2000;
self.slowFactor = 0.5;
LK.effects.flashObject(self, 0x00FF00, 200);
}
// Fast: 10% chance to confuse (random direction) for 1s
if (weaponType === 'fast' && Math.random() < 0.10) {
self.confusedUntil = Date.now() + 1000;
LK.effects.flashObject(self, 0xFF00FF, 120);
}
// Burn: apply burning status
if (weaponType === 'burn') {
self.burningUntil = Date.now() + 2500;
self.burnTick = Date.now();
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 60, "BURN", 0xFF6600);
}
LK.effects.flashObject(self, 0xFF6600, 200);
}
// Freeze: apply slow
if (weaponType === 'freeze') {
self.slowedUntil = Date.now() + 2500;
self.slowFactor = 0.3;
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 60, "FREEZE", 0x66CCFF);
}
LK.effects.flashObject(self, 0x66CCFF, 200);
}
// Poison: apply poison status
if (weaponType === 'poison') {
self.poisonedUntil = Date.now() + 4000;
self.poisonTick = Date.now();
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 60, "POISON", 0x00FF66);
}
LK.effects.flashObject(self, 0x00FF66, 200);
}
// ShieldPierce: remove shield if present
if (weaponType === 'shieldPierce' && self.hasShield) {
self.hasShield = false;
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 60, "SHIELD BREAK", 0xFFFF00);
}
LK.effects.flashObject(self, 0xFFFF00, 400);
}
// Default flash
if (!isCritical && weaponType === 'basic') {
LK.effects.flashObject(self, 0xffffff, 200);
}
}
// --- End weapon-specific hit reactions ---
if (self.hp <= 0) {
self.die();
}
};
self.die = function () {
// Fade out shadow when enemy dies
tween(self.shadow.graphics, {
alpha: 0
}, {
duration: 300
});
// Rotate enemy to make it lie on its side (90 degrees in radians)
tween(self.graphics, {
rotation: Math.PI / 2,
alpha: 0.2
}, {
duration: 500,
easing: tween.easeOut
});
// After rotation, fade out completely
LK.setTimeout(function () {
if (self.graphics) {
tween(self.graphics, {
alpha: 0
}, {
duration: 800,
easing: tween.easeIn
});
}
}, 400);
self.active = false;
var currentScore = LK.getScore();
var scoreMultiplier = 1;
// Increase rewards based on score progression
if (currentScore > 300) {
scoreMultiplier = 2.5;
} else if (currentScore > 200) {
scoreMultiplier = 2;
} else if (currentScore > 100) {
scoreMultiplier = 1.5;
}
LK.setScore(currentScore + self.points);
// Set different currency values based on enemy type with score multiplier
if (self.type === 'regular') {
currency += Math.ceil((3 + 2) * scoreMultiplier);
} else if (self.type === 'fast') {
currency += Math.ceil((4 + 2) * scoreMultiplier);
} else if (self.type === 'tank') {
currency += Math.ceil((6 + 2) * scoreMultiplier);
} else {
currency += Math.ceil((self.currency + 2) * scoreMultiplier); // Fallback
}
updateUI();
};
self.startAttackingWall = function (wall) {
self.isAttackingWall = true;
self.attackTarget = wall;
self.attackAnimationTicks = 0;
};
self.attackBunker = function () {
bunkerHealth -= self.damage;
updateBunkerHealth();
LK.getSound('bunkerHit').play();
LK.effects.flashObject(bunker, 0xff0000, 500);
// Show floating damage number on bunker
if (typeof showDamageNumber === "function") {
showDamageNumber(bunker.x, bunker.y - 80, self.damage, 0xFF8800);
}
// Make damage indicator visible when taking damage
bunker.damageIndicator.alpha = 1;
self.active = false;
// Check if bunker is destroyed
if (bunkerHealth <= 0) {
gameOver();
}
};
return self;
});
var MuzzleFlash = Container.expand(function () {
var self = Container.call(this);
// Add shadow under muzzle flash
self.shadow = new Shadow();
self.shadow.y = 8;
self.addChildAt(self.shadow, 0);
if (self.graphics) {
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.scale.x = baseWidth * 0.8 / 100;
self.shadow.graphics.alpha = 0.5;
}
// Create the flash using bullet shape with yellow-white tint
self.graphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0xFFFF99 // Bright yellow-white color for flash
});
// Initially scale small
self.graphics.scale.set(0.3, 0.3);
self.graphics.alpha = 0.9;
// Animation lifetime
self.duration = 100; // ms
self.startTime = 0;
self.active = false;
// Start the flash effect
self.flash = function () {
self.active = true;
self.startTime = Date.now();
self.visible = true;
// Reset and start animation
self.graphics.scale.set(0.8, 0.8);
self.graphics.alpha = 1;
};
// Update the flash animation
self.update = function () {
if (!self.active) return;
var elapsed = Date.now() - self.startTime;
var progress = elapsed / self.duration;
if (progress >= 1) {
// Animation complete
self.active = false;
self.visible = false;
return;
}
// Animate scale and alpha
self.graphics.scale.set(0.8 * (1 - progress), 0.8 * (1 - progress));
self.graphics.alpha = 1 - progress;
};
// Hide initially
self.visible = false;
return self;
});
var PlaceableWall = Container.expand(function (buildingType, buildingHealth) {
var self = Container.call(this);
// Add shadow under placeable wall
self.shadow = new Shadow();
self.shadow.y = 60;
self.addChildAt(self.shadow, 0);
if (self.graphics) {
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.scale.x = baseWidth * 0.8 / 100;
self.shadow.graphics.alpha = 0.5;
}
// Create semi-transparent wall based on type
if (buildingType === 'reinforced') {
self.graphics = self.attachAsset('reinforcedWall', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (buildingType === 'sniper') {
// Create a tower
self.graphics = self.attachAsset('sniperTower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// No need to add extra sniper on top since it's included in the image
} else {
// Basic wall
self.graphics = self.attachAsset('basicWall', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
}
// Make it semi-transparent
self.alpha = 0.7;
// Store building properties for when it's placed
self.buildingType = buildingType;
self.buildingHealth = buildingHealth;
return self;
});
// Security: Melee security guard with walk animation (like Ally/Enemy)
var Security = Container.expand(function () {
var self = Container.call(this);
// Add shadow under security
self.shadow = new Shadow();
self.shadow.y = 48;
self.addChildAt(self.shadow, 0);
if (self.graphics) {
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.alpha = 0.5;
}
// Use ally soldier image for security for now (replace with unique asset if available)
self.graphics = self.attachAsset('allySoldier', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
self.graphics.alpha = 1;
self.maxHealth = 120;
self.health = self.maxHealth;
self.damage = 2;
self.speed = 2.8;
self.attackRange = 90;
self.attackCooldown = 700;
self.walkAnimTick = 0;
self.lastMoveDirection = 1;
self.updateWalkAnim = function () {
// Simulate walk animation (same as Ally/Enemy)
if (LK.ticks % 10 === 0 && self.active) {
var dir = 1;
if (self.targetEnemy) {
dir = self.targetEnemy.x > self.x ? 1 : -1;
}
self.lastMoveDirection = dir;
tween(self.graphics, {
rotation: dir * 0.1
}, {
duration: 250,
easing: tween.easeInOut
});
}
var walkCycle = Math.sin(LK.ticks * 0.15) * 6;
self.graphics.y = walkCycle;
};
self.lastAttack = 0;
self.targetEnemy = null;
self.followDistance = 100;
self.active = true;
// Health bar background
self.healthBarBg = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x000000,
scaleX: 1.6,
scaleY: 0.22
});
self.healthBarBg.y = 60;
self.addChild(self.healthBarBg);
// Health bar fill
self.healthBar = LK.getAsset('bullet', {
anchorX: 0,
anchorY: 0.5,
tint: 0x00FF00,
scaleX: 1.5,
scaleY: 0.18
});
self.healthBar.y = 60;
self.healthBar.x = -self.healthBarBg.width / 2;
self.addChild(self.healthBar);
// Update health bar
self.updateHealthBar = function () {
var percent = Math.max(0, self.health / self.maxHealth);
self.healthBar.scale.x = 1.5 * percent;
if (percent < 0.3) {
self.healthBar.tint = 0xFF0000;
} else if (percent < 0.6) {
self.healthBar.tint = 0xFFFF00;
} else {
self.healthBar.tint = 0x00FF00;
}
self.healthBar.visible = percent < 1;
self.healthBarBg.visible = percent < 1;
};
// Take damage
self.takeDamage = function (amount) {
self.health -= amount;
self.updateHealthBar();
LK.effects.flashObject(self, 0xff0000, 200);
// Show floating damage number
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 40, amount, 0x00FFFF);
}
if (self.health <= 0) {
self.active = false;
tween(self, {
alpha: 0
}, {
duration: 400
});
self.visible = false;
}
};
// Security update: follow sniper or attack nearest enemy
self.update = function () {
if (!self.active) return;
// Prevent security from dying if it moves off the top or sides of the screen
if (self.x < 50) self.x = 50;
if (self.x > 2048 - 50) self.x = 2048 - 50;
if (self.y < -100) self.y = -100;
self.updateHealthBar();
self.updateWalkAnim();
// Find nearest enemy in range
var nearest = null;
var minDist = 99999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (!e.active) continue;
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearest = e;
}
}
var now = Date.now();
if (nearest) {
self.targetEnemy = nearest;
var angle = Math.atan2(nearest.y - self.y, nearest.x - self.x);
if (minDist > self.attackRange * 0.7) {
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
if (minDist <= self.attackRange && now - self.lastAttack > self.attackCooldown) {
self.lastAttack = now;
// 10% chance to stun, 10% chance to slow, 5% chance to confuse, 10% crit
var isCrit = Math.random() < 0.10;
var effectRoll = Math.random();
if (isCrit) {
nearest.takeDamage(self.damage * 2, 'basic', true);
} else if (effectRoll < 0.10) {
nearest.takeDamage(self.damage, 'stun');
} else if (effectRoll < 0.20) {
nearest.takeDamage(self.damage, 'slow');
} else if (effectRoll < 0.25) {
nearest.takeDamage(self.damage, 'confuse');
} else {
nearest.takeDamage(self.damage, 'basic');
}
LK.effects.flashObject(nearest, 0x00FFFF, 120);
}
} else {
self.targetEnemy = null;
var dx = sniper.x - self.x;
var dy = sniper.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > self.followDistance) {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed * 0.7;
self.y += Math.sin(angle) * self.speed * 0.7;
}
}
};
return self;
});
var Shadow = Container.expand(function () {
var self = Container.call(this);
// Create shadow using dedicated shadow asset
self.graphics = self.attachAsset('shadow', {
anchorX: 0.5,
anchorY: 0.5
});
// Set shadow properties
self.graphics.alpha = 0.5; // More visible shadow
// Method to update shadow size based on parent
self.updateSize = function (parentWidth) {
// Make shadow larger and rounder
self.graphics.scale.x = parentWidth * 1.1 / 100;
self.graphics.scale.y = 0.7; // More circular/ellipse
};
// Default scale for initial appearance
self.graphics.scale.x = 1.1;
self.graphics.scale.y = 0.7;
return self;
});
var Sniper = Container.expand(function () {
var self = Container.call(this);
// Add shadow under sniper
self.shadow = new Shadow();
self.shadow.y = 40;
self.addChildAt(self.shadow, 0);
if (self.graphics) {
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.scale.x = baseWidth * 0.8 / 100;
self.shadow.graphics.alpha = 0.5;
}
self.graphics = self.attachAsset('sniper', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
// Add rifle that will rotate toward cursor
self.rifle = self.attachAsset('rifle', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7
});
// Create references to all weapon graphics but only show the active one
self.weapons = {
basic: self.rifle,
sniper: LK.getAsset('sniper_rifle', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
}),
"super": LK.getAsset('sniper_rifle', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5,
tint: 0xFF00FF // Magenta tint for Super Sniper
})
};
// Add other weapons but hide them initially
self.weapons.sniper.visible = false;
self.weapons.sniper.x = 10;
self.addChild(self.weapons.sniper);
self.weapons["super"].visible = false;
self.weapons["super"].x = 10;
self.addChild(self.weapons["super"]);
// Position rifle to appear like it's being held by the sniper
self.rifle.x = 10;
// Initialize weapon positions
Object.keys(self.weapons).forEach(function (key) {
// Standard positioning
self.weapons[key].x = 10;
self.weapons[key].y = 0;
if (key === 'basic') {
self.weapons[key].scale.x = 1.7;
self.weapons[key].scale.y = 1.7;
} else if (key === 'sniper') {
self.weapons[key].scale.x = 1.2;
self.weapons[key].scale.y = 1.2;
} else {
self.weapons[key].scale.x = 1.7;
self.weapons[key].scale.y = 1.7;
}
});
// Create muzzle flash effects for each weapon
self.muzzleFlashes = {};
Object.keys(self.weapons).forEach(function (key) {
var flash = new MuzzleFlash();
self.weapons[key].addChild(flash);
// Position at the end of each weapon
flash.x = self.weapons[key].width - 10;
flash.y = 0;
if (key === 'super') {
flash.graphics.tint = 0xFF00FF; // Magenta flash for super sniper
}
self.muzzleFlashes[key] = flash;
});
self.fireRate = 1000; // ms between shots
self.lastShot = 0;
self.bulletDamage = 1;
self.bulletSpeed = 15; // Default bullet speed
self.currentWeapon = 'basic'; // Track current weapon type
// Method to update rifle rotation based on cursor position
self.updateAim = function (targetX, targetY) {
// Calculate angle to target
var angle = Math.atan2(targetY - self.y, targetX - self.x);
// Determine if target is on left or right side of the screen
var isTargetOnLeftSide = targetX < 2048 / 2;
// Update sniper graphics based on which side target is on
if (isTargetOnLeftSide) {
// Target is on left side - character faces left
self.graphics.scale.x = -1; // Flip character horizontally
// Adjust weapon positions for left-facing stance
Object.keys(self.weapons).forEach(function (key) {
self.weapons[key].x = -10; // Offset for left side
self.weapons[key].scale.x = -1; // Flip weapon horizontally
});
} else {
// Target is on right side - character faces right
self.graphics.scale.x = 1; // Normal orientation
// Adjust weapon positions for right-facing stance
Object.keys(self.weapons).forEach(function (key) {
self.weapons[key].x = 10; // Normal position on right side
self.weapons[key].scale.x = 1; // Normal weapon orientation
});
}
// Set all weapons rotation to aim at target, properly adjusted for direction
if (isTargetOnLeftSide) {
// When facing left, we need to adjust the angle
self.weapons.basic.rotation = angle + Math.PI;
self.weapons.sniper.rotation = angle + Math.PI;
self.weapons["super"].rotation = angle + Math.PI;
} else {
self.weapons.basic.rotation = angle;
self.weapons.sniper.rotation = angle;
self.weapons["super"].rotation = angle;
}
return angle;
};
self.canShoot = function () {
var now = Date.now();
if (now - self.lastShot >= self.fireRate) {
self.lastShot = now;
return true;
}
return false;
};
self.switchWeapon = function (weaponIndex) {
// Hide all weapons first
self.weapons.basic.visible = false;
self.weapons.sniper.visible = false;
self.weapons["super"].visible = false;
// Show selected weapon based on index
switch (weaponIndex) {
case 0:
self.weapons.basic.visible = true;
self.currentWeapon = 'basic';
break;
case 1:
self.weapons.sniper.visible = true;
self.currentWeapon = 'sniper';
break;
case 2:
self.weapons["super"].visible = true;
self.currentWeapon = 'super';
break;
default:
self.weapons.basic.visible = true;
self.currentWeapon = 'basic';
}
// Reset magazine if switching weapon
var mag = magazines[self.currentWeapon];
if (mag && mag.current > mag.max) mag.current = mag.max;
updateAmmoUI();
};
self.shoot = function (targetX, targetY) {
// Magazine and reload logic
var weapon = self.currentWeapon;
var mag = magazines[weapon];
if (mag.reloading) return null;
if (mag.current <= 0) {
// Start reload
mag.reloading = true;
updateAmmoUI();
// Animate reload: move weapon down and up, and show "reloading..." in UI
tween(self.weapons[weapon], {
y: 60,
alpha: 0.5
}, {
duration: Math.floor(mag.reloadTime * 0.4),
easing: tween.easeIn,
onFinish: function onFinish() {
tween(self.weapons[weapon], {
y: 0,
alpha: 1
}, {
duration: Math.floor(mag.reloadTime * 0.6),
easing: tween.easeOut
});
}
});
LK.setTimeout(function () {
mag.current = mag.max;
mag.reloading = false;
updateAmmoUI();
}, mag.reloadTime);
return null;
}
if (!self.canShoot()) return null;
mag.current--;
updateAmmoUI();
// Update rifle aim
var angle = self.updateAim(targetX, targetY);
var bullet = new Bullet();
// Determine if target is on left or right side of the screen
var isTargetOnLeftSide = targetX < 2048 / 2;
var activeWeapon = self.weapons[self.currentWeapon];
var rifleLength = activeWeapon.width;
// Calculate bullet spawn position at the muzzle, always at the tip of the rifle in world space
// Get the local muzzle position (rifle tip) in weapon's local space
var muzzleLocalX = rifleLength;
var muzzleLocalY = 0;
// Transform muzzle position to world space
// 1. Get weapon's rotation and scale.x (for left/right flip)
var weaponRotation = activeWeapon.rotation;
var weaponScaleX = activeWeapon.scale.x;
// 2. Calculate rotated and flipped muzzle position
var cosR = Math.cos(weaponRotation);
var sinR = Math.sin(weaponRotation);
var muzzleWorldX = self.x + (activeWeapon.x + muzzleLocalX * weaponScaleX) * cosR - muzzleLocalY * sinR;
var muzzleWorldY = self.y + (activeWeapon.y + muzzleLocalX * weaponScaleX) * sinR + muzzleLocalY * cosR;
// Place bullet at the muzzle tip
bullet.x = muzzleWorldX;
bullet.y = muzzleWorldY;
// Set bullet direction - always use the original angle for bullet direction
bullet.speedX = Math.cos(angle) * (self.currentWeapon === "super" ? self.bulletSpeed : bullet.speed);
bullet.speedY = Math.sin(angle) * (self.currentWeapon === "super" ? self.bulletSpeed : bullet.speed);
bullet.damage = self.bulletDamage;
bullet.speed = self.bulletSpeed; // Set bullet speed from sniper
// Customize bullet appearance based on weapon type
if (self.currentWeapon === 'sniper') {
bullet.graphics.tint = 0x33CCFF; // Blue tint for sniper bullets
bullet.graphics.scale.set(0.8, 1.5); // Thinner, longer bullets
bullet.weaponType = 'sniper';
} else if (self.currentWeapon === 'super') {
bullet.graphics.tint = 0xFF00FF; // Magenta tint for super sniper bullets
bullet.graphics.scale.set(1.2, 2.2); // Even longer, more powerful look
bullet.damage = self.bulletDamage; // Ensure correct damage
bullet.speed = self.bulletSpeed; // Ensure correct speed
bullet.weaponType = 'super';
} else if (self.currentWeapon === 'tank') {
bullet.graphics.tint = 0x00FF00; // Green for tank
bullet.graphics.scale.set(1.5, 1.5);
bullet.damage = self.bulletDamage;
bullet.speed = self.bulletSpeed;
bullet.weaponType = 'tank';
} else if (self.currentWeapon === 'fast') {
bullet.graphics.tint = 0xFF00FF; // Magenta for fast
bullet.graphics.scale.set(0.5, 0.7);
bullet.damage = self.bulletDamage;
bullet.speed = self.bulletSpeed;
bullet.weaponType = 'fast';
} else if (self.currentWeapon === 'burn') {
bullet.graphics.tint = 0xFF6600;
bullet.graphics.scale.set(1.1, 1.1);
bullet.weaponType = 'burn';
} else if (self.currentWeapon === 'freeze') {
bullet.graphics.tint = 0x66CCFF;
bullet.graphics.scale.set(1.1, 1.1);
bullet.weaponType = 'freeze';
} else if (self.currentWeapon === 'poison') {
bullet.graphics.tint = 0x00FF66;
bullet.graphics.scale.set(1.1, 1.1);
bullet.weaponType = 'poison';
} else if (self.currentWeapon === 'shieldPierce') {
bullet.graphics.tint = 0xFFFF00;
bullet.graphics.scale.set(1.1, 1.1);
bullet.weaponType = 'shieldPierce';
}
// Trigger muzzle flash for current weapon
var muzzleFlash = self.muzzleFlashes[self.currentWeapon];
if (muzzleFlash) {
muzzleFlash.flash();
}
// Add weapon recoil effect based on weapon type
var recoilDistance = 10; // Default recoil for rifle
var recoilDuration = 100; // Default recoil recovery time
var originX = activeWeapon.x;
// Set different recoil parameters based on weapon type
if (self.currentWeapon === 'sniper') {
recoilDistance = 20; // Stronger recoil for sniper
recoilDuration = 150;
} else if (self.currentWeapon === 'super') {
recoilDistance = 35; // Even stronger recoil for super sniper
recoilDuration = 200;
}
// Calculate recoil direction (opposite to shot direction)
// For recoil, we need to consider which direction the weapon is facing
var recoilDirection = isTargetOnLeftSide ? 1 : -1; // Reverse for left-facing
var bulletAngle = angle; // Use the same angle as bullet direction
var recoilX = recoilDirection * Math.cos(bulletAngle) * recoilDistance;
var recoilY = -Math.sin(bulletAngle) * recoilDistance;
// Apply recoil to the active weapon
tween(activeWeapon, {
x: originX + recoilX,
y: recoilY
}, {
duration: 50,
// Quick recoil
easing: tween.easeOut,
onFinish: function onFinish() {
// Recover from recoil
tween(activeWeapon, {
x: originX,
y: 0
}, {
duration: recoilDuration,
easing: tween.easeInOut
});
}
});
LK.getSound('shoot').play();
return bullet;
};
return self;
});
var Tree = Container.expand(function () {
var self = Container.call(this);
// Random tree appearance with variation
var type = Math.floor(Math.random() * 3);
var size = Math.random() * 0.7 + 1.2; // Size between 1.2 and 1.9
var rotation = Math.random() * 0.2 - 0.1; // Slight rotation
// Create the tree trunk using bullet shape with brown tint
var treeTrunk = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x8B4513,
// Saddle brown color
scaleX: size * 0.3,
scaleY: size * 0.8
});
// Create the tree crown using bullet shape with green tint
var treeCrown = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x228B22,
// Forest green color
scaleX: size * 1.2,
scaleY: size * 1.0
});
// Position crown on top of trunk
treeCrown.y = -treeTrunk.height * 0.5;
// Apply random rotation
self.rotation = rotation;
// Add some details to make trees look different from each other
if (type === 0) {
// First type - taller tree with darker green
treeCrown.scale.y *= 1.4;
treeCrown.tint = 0x006400; // Dark green
} else if (type === 1) {
// Second type - wider tree with lighter green
treeCrown.scale.x *= 1.3;
treeCrown.tint = 0x32CD32; // Lime green
} else {
// Third type - autumn tree with different color
treeCrown.tint = 0xFF8C00; // Dark orange
}
return self;
});
var Wall = Container.expand(function (type, health) {
var self = Container.call(this);
// Add shadow under wall
self.shadow = new Shadow();
self.shadow.y = 60;
self.addChildAt(self.shadow, 0);
if (self.graphics) {
var baseWidth = self.graphics.width * (self.graphics.scale ? self.graphics.scale.x : 1);
if (self.shadow.updateSize) self.shadow.updateSize(baseWidth);
self.shadow.x = 0;
self.shadow.graphics.scale.x = baseWidth * 0.8 / 100;
self.shadow.graphics.alpha = 0.5;
}
// Set properties based on wall type
self.type = type || 'basic';
self.maxHealth = health || 100;
self.health = self.maxHealth;
// Create wall graphic based on type
if (self.type === 'reinforced') {
self.graphics = self.attachAsset('reinforcedWall', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.type === 'sniper') {
// Create a tower
self.graphics = self.attachAsset('sniperTower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// Sniper tower stats
self.maxHealth = 300;
self.health = self.maxHealth;
self.towerDamage = 2;
self.towerRange = 700;
self.towerFireRate = 1200; // ms between shots
self.towerLastShot = 0;
self.towerTarget = null;
self.towerBulletSpeed = 18;
self.towerBullet = function (target) {
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y - self.graphics.height / 2;
var dx = target.x - bullet.x;
var dy = target.y - bullet.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// Prevent division by zero and ensure bullet always moves
if (dist === 0) dist = 0.0001;
bullet.speedX = dx / dist * self.towerBulletSpeed;
bullet.speedY = dy / dist * self.towerBulletSpeed;
bullet.damage = self.towerDamage;
bullet.graphics.tint = 0x33CCFF;
bullet.graphics.scale.set(0.7, 1.3);
return bullet;
};
// No need to add extra sniper on top since it's included in the image
} else {
// Basic wall
self.graphics = self.attachAsset('basicWall', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
}
// Add health bar
self.healthBar = new Container();
self.addChild(self.healthBar);
// Health bar background
self.healthBarBg = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x000000,
scaleX: 3,
scaleY: 0.3
});
self.healthBarBg.y = -40;
self.healthBar.addChild(self.healthBarBg);
// Health bar fill
self.healthBarFill = LK.getAsset('bullet', {
anchorX: 0,
anchorY: 0.5,
tint: 0x00FF00,
scaleX: 3,
scaleY: 0.2
});
self.healthBarFill.y = -40;
self.healthBarFill.x = -self.healthBarBg.width / 2;
self.healthBar.addChild(self.healthBarFill);
// Hide health bar initially
self.healthBar.visible = false;
self.update = function () {
// Show health bar if damaged, and always show for sniper tower if not full health
if (self.health < self.maxHealth || self.type === 'sniper' && self.health < self.maxHealth) {
self.healthBar.visible = true;
// Update health bar fill
var healthPercent = self.health / self.maxHealth;
self.healthBarFill.scale.x = 3 * healthPercent;
// Change color based on health
if (healthPercent < 0.3) {
self.healthBarFill.tint = 0xFF0000; // Red
} else if (healthPercent < 0.6) {
self.healthBarFill.tint = 0xFFFF00; // Yellow
} else {
self.healthBarFill.tint = 0x00FF00; // Green
}
} else {
self.healthBar.visible = false;
}
// Sniper tower AI: shoot at nearest enemy in range
if (self.type === 'sniper' && self.health > 0) {
var now = Date.now();
// Find nearest enemy in range
var nearest = null;
var minDist = 99999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (!e.active) continue;
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist && dist < self.towerRange) {
minDist = dist;
nearest = e;
}
}
if (nearest && now - self.towerLastShot > self.towerFireRate) {
self.towerLastShot = now;
var bullet = self.towerBullet(nearest);
if (bullet) {
bullets.push(bullet);
if (game && typeof game.addChild === "function") game.addChild(bullet);
}
}
}
};
self.takeDamage = function (amount) {
self.health -= amount;
// Show floating damage number on wall
if (typeof showDamageNumber === "function") {
showDamageNumber(self.x, self.y - 40, amount, 0xFFAA00);
}
if (self.health <= 0) {
self.destroy();
return true; // Wall destroyed
}
return false; // Wall still standing
};
return self;
});
var WaveCountdown = Container.expand(function () {
var self = Container.call(this);
self.countdownTime = 60; // 60 seconds default
self.active = false;
// Create countdown text
self.countdownText = new Text2(self.countdownTime, {
size: 150,
fill: 0xFFFFFF
});
self.countdownText.anchor.set(0.5, 0.5);
self.addChild(self.countdownText);
// Create skip button
self.skipButton = new Text2("SKIP", {
size: 80,
fill: 0xFFFF00
});
self.skipButton.anchor.set(0.5, 0.5);
self.skipButton.y = 100;
self.addChild(self.skipButton);
// Skip button background for better visibility
var skipButtonBg = LK.getAsset('bullet', {
width: 200,
height: 100,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
skipButtonBg.alpha = 0.8;
skipButtonBg.y = 100;
self.addChild(skipButtonBg);
// Custom swap implementation since swapChildren is not available
var parent = self.skipButton.parent;
var skipButtonIndex = parent.children.indexOf(skipButtonBg);
var buttonIndex = parent.children.indexOf(self.skipButton);
// Manual reordering of children to create swap effect
if (skipButtonIndex !== -1 && buttonIndex !== -1) {
// Remove both from parent
parent.removeChild(skipButtonBg);
parent.removeChild(self.skipButton);
// Add them back in reverse order
parent.addChild(self.skipButton);
parent.addChild(skipButtonBg);
}
// Timer for countdown
self.timer = null;
// Start countdown
self.startCountdown = function (onComplete) {
self.active = true;
self.visible = true;
self.countdownTime = 60;
self.countdownText.setText(self.countdownTime);
self.onComplete = onComplete;
// Update countdown every second
self.timer = LK.setInterval(function () {
self.countdownTime--;
self.countdownText.setText(self.countdownTime);
// Update next wave timer UI if available
if (typeof nextWaveTimerTxt !== "undefined") {
nextWaveTimerTxt.setText('Next Wave: ' + self.countdownTime + 's');
}
if (self.countdownTime <= 0) {
self.stopCountdown();
if (self.onComplete) self.onComplete();
}
}, 1000);
};
// Stop countdown
self.stopCountdown = function () {
if (self.timer) {
LK.clearInterval(self.timer);
self.timer = null;
}
self.active = false;
self.visible = false;
};
// Skip button event handler
self.skipButton.down = function (x, y, obj) {
if (self.active) {
self.stopCountdown();
if (self.onComplete) self.onComplete();
}
};
return self;
});
var WeaponShop = Container.expand(function () {
var self = Container.call(this);
self.isOpen = false;
self.weapons = [{
name: "Basic Rifle",
price: 0,
damage: 1,
speed: 15,
owned: true,
weaponType: "basic"
}, {
name: "Sniper Rifle",
price: 50,
damage: 3,
speed: 30,
owned: false,
weaponType: "sniper"
}, {
name: "Super Sniper",
price: 150,
damage: 7,
speed: 40,
owned: false,
weaponType: "super"
}, {
name: "Tank Gun",
price: 90,
damage: 5,
speed: 18,
owned: false,
weaponType: "tank"
}, {
name: "Fast Blaster",
price: 70,
damage: 2,
speed: 35,
owned: false,
weaponType: "fast"
}, {
name: "Burn Gun",
price: 120,
damage: 2,
speed: 20,
owned: false,
weaponType: "burn"
}, {
name: "Freeze Gun",
price: 120,
damage: 2,
speed: 20,
owned: false,
weaponType: "freeze"
}, {
name: "Poison Gun",
price: 120,
damage: 2,
speed: 20,
owned: false,
weaponType: "poison"
}, {
name: "Shield Piercer",
price: 200,
damage: 3,
speed: 25,
owned: false,
weaponType: "shieldPierce"
}];
// Shop button
self.shopButton = new Text2("WEAPONS", {
size: 80,
fill: 0xFFFF00
});
self.shopButton.anchor.set(0.5, 0);
// Add background to make button more visible
var buttonBg = LK.getAsset('bullet', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
buttonBg.alpha = 0.8;
buttonBg.y = self.shopButton.height / 2;
self.addChild(buttonBg);
self.addChild(self.shopButton);
// Shop panel (hidden by default)
self.panel = new Container();
self.panel.visible = false;
self.addChild(self.panel);
// Create weapon options
self.weaponItems = [];
for (var i = 0; i < self.weapons.length; i++) {
var weapon = self.weapons[i];
var item = new Container();
var bg = LK.getAsset('bullet', {
width: 400,
height: 150,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
bg.alpha = 0.8;
item.addChild(bg);
var title = new Text2(weapon.name, {
size: 40,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0);
title.y = -50;
item.addChild(title);
var info = new Text2("Damage: " + weapon.damage + " | Speed: " + weapon.speed, {
size: 30,
fill: 0xFFFFFF
});
info.anchor.set(0.5, 0);
info.y = 0;
item.addChild(info);
var priceText = new Text2(weapon.owned ? "OWNED" : "$" + weapon.price, {
size: 35,
fill: weapon.owned ? 0x00FF00 : 0xFFFF00
});
priceText.anchor.set(0.5, 0);
priceText.y = 40;
item.addChild(priceText);
item.y = i * 200;
item.weaponIndex = i;
self.weaponItems.push(item);
self.panel.addChild(item);
}
// Position panel
self.panel.y = 250;
// Handle shop button press
self.shopButton.down = function (x, y, obj) {
self.toggleShop();
};
// Toggle shop visibility
self.toggleShop = function () {
self.isOpen = !self.isOpen;
self.panel.visible = self.isOpen;
};
// Handle weapon selection
self.selectWeapon = function (index) {
var weapon = self.weapons[index];
if (weapon.owned) {
// Equip weapon
sniper.bulletDamage = weapon.damage;
sniper.bulletSpeed = weapon.speed;
sniper.currentWeapon = weapon.weaponType || "basic";
sniper.switchWeapon(index); // Switch to selected weapon appearance
// Visual feedback for equipped weapon
for (var i = 0; i < self.weaponItems.length; i++) {
var priceText = self.weaponItems[i].children[3];
if (i === index) {
priceText.setText("EQUIPPED", {
fill: i === 2 ? 0xFF00FF : 0x00FFFF // Magenta for super sniper, cyan for others
});
} else if (self.weapons[i].owned) {
priceText.setText("OWNED", {
fill: i === 2 ? 0xFF00FF : 0x00FF00 // Magenta for super sniper, green for others
});
}
}
LK.getSound('upgrade').play();
return true;
} else if (currency >= weapon.price) {
// Purchase weapon
currency -= weapon.price;
weapon.owned = true;
// Update UI
var priceText = self.weaponItems[index].children[3];
priceText.setText("EQUIPPED", {
fill: index === 2 ? 0xFF00FF : 0x00FFFF // Magenta for super sniper, cyan for others
});
// Reset other weapon texts to "OWNED"
for (var i = 0; i < self.weaponItems.length; i++) {
if (i !== index && self.weapons[i].owned) {
self.weaponItems[i].children[3].setText("OWNED", {
fill: i === 2 ? 0xFF00FF : 0x00FF00 // Magenta for super sniper, green for others
});
}
}
// Equip weapon
sniper.bulletDamage = weapon.damage;
sniper.bulletSpeed = weapon.speed;
sniper.switchWeapon(index); // Switch to selected weapon appearance
LK.getSound('upgrade').play();
updateUI();
return true;
} else {
// Not enough currency
LK.effects.flashScreen(0xFF0000, 300);
return false;
}
};
// Check if an item was clicked
self.checkItemClick = function (x, y) {
if (!self.isOpen) return false;
var pos = self.toLocal({
x: x,
y: y
});
for (var i = 0; i < self.weaponItems.length; i++) {
var item = self.weaponItems[i];
// Use item's actual bounds for hit detection
var bounds = item.children[0].getBounds(); // children[0] is the background
// Convert bounds to local coordinates relative to the panel
var itemLeft = item.x + bounds.x - bounds.width * item.children[0].anchorX;
var itemRight = itemLeft + bounds.width;
var itemTop = item.y + bounds.y - bounds.height * item.children[0].anchorY;
var itemBottom = itemTop + bounds.height;
if (pos.x >= itemLeft && pos.x <= itemRight && pos.y >= itemTop && pos.y <= itemBottom) {
return self.selectWeapon(item.weaponIndex);
}
}
return false;
};
// Add down event handlers to each weapon item
for (var i = 0; i < self.weaponItems.length; i++) {
var item = self.weaponItems[i];
item.interactive = true;
item.index = i; // Store the index
// Using custom event handler for each item
(function (index) {
item.down = function (x, y, obj) {
self.selectWeapon(index);
};
})(i);
}
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
// No title, no description
// Always backgroundColor is black
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Dedicated shadow asset for under characters
// Armored Vehicle (Zırhlı Araç) enemy assets for all directions
// Create background
var background = game.attachAsset('background', {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0,
tint: 0xC2B280 // Apply dry land tint to the background
});
// Game variables
var bunkerHealth = 100;
var maxBunkerHealth = 100;
var bunkerY = 2732 - 100; // Position near bottom of screen
var currency = 0;
var difficulty = 1;
var wave = 1;
var enemySpawnRate = 3000; // ms between enemy spawns
var lastEnemySpawn = 0;
var gameActive = true;
var waveInProgress = false;
var enemyIncreasePerWave = 0.25; // 25% more enemies per wave
var enemiesPerWave = 25; // Starting with 25 enemies
var enemiesSpawned = 0; // Track enemies spawned in current wave
var enemiesRequired = 10; // Initial enemies required for first wave
// Start score at 0
LK.setScore(0);
// Magazine system for each weapon type
var magazines = {
basic: {
max: 15,
current: 15,
reloading: false,
reloadTime: 1200
},
sniper: {
max: 10,
current: 10,
reloading: false,
reloadTime: 1800
},
"super": {
max: 5,
current: 5,
reloading: false,
reloadTime: 2500
}
};
// UI for ammo display
var ammoTxt = new Text2('Ammo: 5/5', {
size: 50,
fill: 0xFFFF00
});
ammoTxt.anchor.set(1, 0);
ammoTxt.x = 2048 - 80;
ammoTxt.y = 60;
LK.gui.top.addChild(ammoTxt);
// Helper to update ammo UI
function updateAmmoUI() {
var weapon = sniper.currentWeapon;
var mag = magazines[weapon];
if (mag.reloading) {
ammoTxt.setText('Reloading...');
} else {
ammoTxt.setText('Ammo: ' + mag.current + '/' + mag.max);
}
}
// --- Floating damage numbers ---
// Show floating text at (x, y) with value and color, and special effect for status
function showDamageNumber(x, y, value, color, status) {
var textValue = value;
var textColor = color || 0xFF2222;
var textSize = 48;
var extraTween = null;
var statusEffect = status || "";
// Status effect: show text and color for effect
if (statusEffect === "CRIT") {
textValue = "CRIT " + value;
textColor = 0xFFD700;
textSize = 60;
} else if (statusEffect === "SLOW") {
textValue = "SLOW";
textColor = 0x00FFFF;
textSize = 44;
} else if (statusEffect === "BURN") {
textValue = "BURN";
textColor = 0xFF6600;
textSize = 44;
} else if (statusEffect === "FREEZE") {
textValue = "FREEZE";
textColor = 0x66CCFF;
textSize = 44;
} else if (statusEffect === "POISON") {
textValue = "POISON";
textColor = 0x00FF66;
textSize = 44;
} else if (statusEffect === "SHIELD") {
textValue = "SHIELD";
textColor = 0xFFFF00;
textSize = 44;
} else if (statusEffect === "SHIELD BREAK") {
textValue = "SHIELD BREAK";
textColor = 0xFFFF00;
textSize = 44;
}
var dmgTxt = new Text2('-' + textValue, {
size: textSize,
fill: textColor
});
dmgTxt.anchor.set(0.5, 0.5);
dmgTxt.x = x;
dmgTxt.y = y;
dmgTxt.alpha = 1;
game.addChild(dmgTxt);
// Animate up and fade out, with a little scale pop for crit/status
var scaleFrom = statusEffect === "CRIT" ? 1.5 : 1.1;
dmgTxt.scale.set(scaleFrom, scaleFrom);
tween(dmgTxt.scale, {
x: 1,
y: 1
}, {
duration: 200,
easing: tween.easeOut
});
tween(dmgTxt, {
y: y - 60,
alpha: 0
}, {
duration: 700,
easing: tween.easeOut,
onFinish: function onFinish() {
game.removeChild(dmgTxt);
}
});
}
// Upgrade costs and values
var upgrades = {
fireRate: {
level: 1,
cost: 10,
value: 1000,
// ms between shots
increment: -100 // decrease time between shots
},
bulletDamage: {
level: 1,
cost: 15,
value: 1,
increment: 1 // increase damage
}
};
// Arrays for tracking game objects
var bullets = [];
var enemies = [];
var walls = [];
// Ally instance (semi-transparent melee ally)
var ally = null;
// Create bunker
var bunker = new Bunker();
bunker.x = 2048 / 2;
bunker.y = bunkerY;
game.addChild(bunker);
// Create sniper
var sniper = new Sniper();
sniper.x = 2048 / 2;
sniper.y = bunkerY - 50;
game.addChild(sniper);
// Create weapon shop
var weaponShop = new WeaponShop();
weaponShop.x = 350; // Moved more to the right for better visibility
weaponShop.y = 1800; // Position higher on the screen for better visibility
game.addChild(weaponShop); // Add to game instead of GUI for better positioning
// Create building shop
var buildingShop = new BuildingShop();
buildingShop.x = 400; // Moved further right to improve visibility and hit detection
buildingShop.y = 600; // Repositioned lower on the screen for better accessibility
game.addChild(buildingShop);
// Create wave countdown timer
var waveCountdown = new WaveCountdown();
waveCountdown.x = 2048 / 2; // Center horizontally
waveCountdown.y = 2732 / 2; // Center vertically
waveCountdown.visible = false; // Hide initially
game.addChild(waveCountdown);
// UI Elements
// Score display
var scoreTxt = new Text2('Score: 0', {
size: 70,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
scoreTxt.y = 50;
LK.gui.top.addChild(scoreTxt);
// --- Mini-map UI ---
// Mini-map config
var minimapWidth = 320;
var minimapHeight = 420;
var minimapScaleX = minimapWidth / 2048;
var minimapScaleY = minimapHeight / 2732;
var minimapMargin = 40;
// Mini-map container
var minimapContainer = new Container();
minimapContainer.x = 2048 - minimapWidth - minimapMargin;
minimapContainer.y = 2732 - minimapHeight - minimapMargin;
minimapContainer.alpha = 0.85;
// Mini-map background
var minimapBg = LK.getAsset('bullet', {
width: minimapWidth,
height: minimapHeight,
anchorX: 0,
anchorY: 0,
tint: 0x222222
});
minimapBg.alpha = 0.7;
minimapContainer.addChild(minimapBg);
// Mini-map bunker marker
var minimapBunker = LK.getAsset('bullet', {
width: 32,
height: 32,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00FF00
});
minimapBunker.alpha = 0.9;
minimapContainer.addChild(minimapBunker);
// Mini-map enemy markers (will be updated every frame)
var minimapEnemyMarkers = [];
// Add to game (not GUI, so it scales with world)
game.addChild(minimapContainer);
// Mini-map update function
function updateMinimap() {
// Update bunker marker
minimapBunker.x = bunker.x * minimapScaleX;
minimapBunker.y = bunker.y * minimapScaleY;
// Remove old enemy markers
for (var i = 0; i < minimapEnemyMarkers.length; i++) {
minimapContainer.removeChild(minimapEnemyMarkers[i]);
}
minimapEnemyMarkers = [];
// Add enemy markers
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (!e.active) continue;
var marker = LK.getAsset('bullet', {
width: 18,
height: 18,
anchorX: 0.5,
anchorY: 0.5,
tint: e.type === "armoredVehicle" ? 0xFF8800 : e.type === "tank" ? 0xFF0000 : 0xFFFF00
});
marker.x = e.x * minimapScaleX;
marker.y = e.y * minimapScaleY;
marker.alpha = 0.95;
minimapContainer.addChild(marker);
minimapEnemyMarkers.push(marker);
}
}
// Super Ally button UI (above bomb button)
var superAllyBtnBg = LK.getAsset('bullet', {
width: 260,
height: 110,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
superAllyBtnBg.alpha = 0.8;
var superAllyBtn = new Text2("SUPER ALLY\n$1000", {
size: 48,
fill: 0xFF00FF
});
superAllyBtn.anchor.set(0.5, 0.5);
var superAllyBtnContainer = new Container();
superAllyBtnContainer.addChild(superAllyBtnBg);
superAllyBtnContainer.addChild(superAllyBtn);
superAllyBtnContainer.x = 140;
superAllyBtnContainer.y = 2732 - 410; // Above bomb button
superAllyBtnContainer.interactive = true;
superAllyBtnContainer.visible = true;
game.addChild(superAllyBtnContainer);
// Bomb button UI (above ally button)
var bombBtnBg = LK.getAsset('bullet', {
width: 260,
height: 110,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
bombBtnBg.alpha = 0.8;
var bombBtn = new Text2("BOMB\n$50", {
size: 55,
fill: 0xFFD700
});
bombBtn.anchor.set(0.5, 0.5);
var bombBtnContainer = new Container();
bombBtnContainer.addChild(bombBtnBg);
bombBtnContainer.addChild(bombBtn);
bombBtnContainer.x = 140;
bombBtnContainer.y = 2732 - 280; // Above ally button
bombBtnContainer.interactive = true;
bombBtnContainer.visible = true;
game.addChild(bombBtnContainer);
// Super Ally effect state
var superAllyActive = false;
var superAllyCooldown = false;
// Bomb state
var bombMode = false;
var bombBtnCooldown = false;
// Super Ally button event
superAllyBtnContainer.down = function (x, y, obj) {
if (superAllyCooldown) return;
if (currency >= 1000 && !superAllyActive) {
currency -= 1000;
updateUI();
superAllyActive = true;
superAllyBtnBg.alpha = 0.4;
superAllyBtn.alpha = 0.5;
// Spawn 10 armored vehicles from back (top) edge, move to front (bottom), destroy all enemies in their path
var superAllyVehicles = [];
var startX = sniper.x - 220; // Spawn behind sniper horizontally
var startY = 0 - 120; // Start above the visible area
var endY = bunkerY - 120; // End just before the bunker
var spacing = (2048 - 400) / 9; // Spread across width, avoid edges
for (var i = 0; i < 10; i++) {
var av = new Enemy("armoredVehicle");
av.hp = 99999; // Invincible
av.damage = 9999;
av.speed = 7; // Slowed down from 32 to 7 for more reasonable speed
av.points = 0;
av.currency = 0;
av.type = "armoredVehicle";
// Use 'front' asset (facing down/forward)
if (av.graphics) av.removeChild(av.graphics);
av.graphics = av.attachAsset('armoredVehicle_front', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7
});
// Add shadow
if (av.shadow) av.removeChild(av.shadow);
av.shadow = new Shadow();
av.shadow.y = 40;
var baseWidth = av.graphics.width * av.graphics.scale.x;
av.shadow.updateSize(baseWidth);
av.addChildAt(av.shadow, 0);
av.shadow.x = 0;
av.shadow.graphics.scale.x = baseWidth * 0.8 / 100;
av.shadow.graphics.alpha = 0.5;
// Spread vehicles horizontally, all start at the back (top)
av.x = 200 + i * spacing;
av.y = startY;
av.superAlly = true; // Mark as super ally
// --- Smoke effect state for this vehicle ---
av.smokeParticles = [];
av.smokeTick = 0;
// --- Super Ally update with smoke effect ---
av.update = function () {
// Move forward (down)
this.y += this.speed;
// --- Smoke effect: spawn smoke every few frames behind the vehicle ---
if (typeof this.smokeTick !== "number") this.smokeTick = 0;
this.smokeTick++;
if (this.smokeTick % 3 === 0) {
// Create a smoke particle (small gray ellipse)
var smoke = LK.getAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.7 + Math.random() * 0.5,
scaleY: 0.5 + Math.random() * 0.3,
tint: 0x888888
});
smoke.x = this.x + (Math.random() - 0.5) * 30;
smoke.y = this.y - 60 + (Math.random() - 0.5) * 10;
smoke.alpha = 0.7 + Math.random() * 0.2;
smoke.life = 0;
smoke.maxLife = 24 + Math.floor(Math.random() * 10);
// Add to game and to this vehicle's smokeParticles array
game.addChildAt(smoke, 0);
this.smokeParticles.push(smoke);
}
// Update and fade out smoke particles
for (var s = this.smokeParticles.length - 1; s >= 0; s--) {
var sp = this.smokeParticles[s];
sp.life++;
sp.y += 1.2 + Math.random() * 0.5; // Drift down
sp.x += (Math.random() - 0.5) * 1.2; // Slight horizontal drift
sp.alpha *= 0.96; // Fade out
sp.scale.x *= 1.01;
sp.scale.y *= 1.01;
if (sp.life > sp.maxLife || sp.alpha < 0.05) {
game.removeChild(sp);
this.smokeParticles.splice(s, 1);
}
}
// Destroy all enemies in path
for (var j = enemies.length - 1; j >= 0; j--) {
var e = enemies[j];
if (e.active && Math.abs(e.x - this.x) < 120 && Math.abs(e.y - this.y) < 80) {
e.die();
}
}
// Remove self if off screen (past bunker)
if (this.y > bunkerY + 200) {
// Remove all smoke particles
for (var s = this.smokeParticles.length - 1; s >= 0; s--) {
game.removeChild(this.smokeParticles[s]);
}
this.smokeParticles = [];
this.active = false;
if (game && typeof game.removeChild === "function") game.removeChild(this);
}
};
superAllyVehicles.push(av);
game.addChild(av);
}
// Animate button cooldown
superAllyCooldown = true;
LK.setTimeout(function () {
superAllyActive = false;
superAllyBtnBg.alpha = 0.8;
superAllyBtn.alpha = 1;
superAllyCooldown = false;
}, 4000);
// Remove all super ally vehicles after 4 seconds
LK.setTimeout(function () {
for (var i = 0; i < superAllyVehicles.length; i++) {
if (superAllyVehicles[i] && game && typeof game.removeChild === "function") {
game.removeChild(superAllyVehicles[i]);
}
}
}, 4200);
} else if (currency < 1000) {
LK.effects.flashObject(superAllyBtnContainer, 0xFF0000, 200);
}
};
// Bomb button event
bombBtnContainer.down = function (x, y, obj) {
if (bombBtnCooldown) return;
if (currency >= 50 && !bombMode) {
bombMode = true;
bombBtnBg.alpha = 0.4;
bombBtn.alpha = 0.5;
} else if (bombMode) {
// Already in bomb mode, ignore
} else {
// Not enough money
LK.effects.flashObject(bombBtnContainer, 0xFF0000, 200);
}
};
// Ally purchase button (bottom left corner, visible)
var allyBtnBg = LK.getAsset('bullet', {
width: 260,
height: 110,
anchorX: 0.5,
anchorY: 0.5,
tint: 0x333333
});
allyBtnBg.alpha = 0.8;
var allyBtn = new Text2("ALLY\n$100", {
size: 55,
fill: 0x00FFFF
});
allyBtn.anchor.set(0.5, 0.5);
var allyBtnContainer = new Container();
allyBtnContainer.addChild(allyBtnBg);
allyBtnContainer.addChild(allyBtn);
allyBtnContainer.x = 140;
allyBtnContainer.y = 2732 - 140;
allyBtnContainer.interactive = true;
allyBtnContainer.visible = true;
game.addChild(allyBtnContainer);
// Ally button event
allyBtnContainer.down = function (x, y, obj) {
if (currency >= 100 && (!ally || !ally.active)) {
currency -= 100;
updateUI();
if (ally && !ally.active) {
game.removeChild(ally);
}
ally = new Ally();
// Spawn behind the sniper (above, on y axis)
ally.x = sniper.x;
ally.y = sniper.y + 120;
game.addChild(ally);
// --- Fix: Reset all enemy walk animation scale to default when ally is created ---
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (e && e.graphics && (e.type === 'fast' || e.type === 'tank' || e.type === 'regular')) {
e.graphics.scale.x = 2.5;
e.graphics.scale.y = 2.5;
}
}
LK.effects.flashObject(ally, 0x00FFFF, 400);
} else if (currency < 100) {
LK.effects.flashObject(allyBtnContainer, 0xFF0000, 200);
}
};
// Wave display
var waveTxt = new Text2('Wave: 1', {
size: 50,
fill: 0xFFFFFF
});
waveTxt.anchor.set(1, 0);
LK.gui.topLeft.addChild(waveTxt);
waveTxt.x = 150; // Move away from the top left corner
// Health display
var healthTxt = new Text2('Bunker: 100%', {
size: 50,
fill: 0xFFFFFF
});
healthTxt.anchor.set(0.5, 0);
healthTxt.y = 130;
LK.gui.top.addChild(healthTxt);
// Enemy counter UI
var enemyCounterTxt = new Text2('Enemies: 0', {
size: 50,
fill: 0xFF4444
});
enemyCounterTxt.anchor.set(1, 0);
enemyCounterTxt.x = 2048 - 80;
enemyCounterTxt.y = 130;
LK.gui.top.addChild(enemyCounterTxt);
// Next wave timer UI
var nextWaveTimerTxt = new Text2('', {
size: 50,
fill: 0x00FFFF
});
nextWaveTimerTxt.anchor.set(1, 0);
nextWaveTimerTxt.x = 2048 - 80;
nextWaveTimerTxt.y = 200;
LK.gui.top.addChild(nextWaveTimerTxt);
// Currency display
var currencyTxt = new Text2('$: 0', {
size: 50,
fill: 0x00FF00
});
currencyTxt.anchor.set(0, 0);
LK.gui.topLeft.addChild(currencyTxt);
currencyTxt.x = 150; // Move away from top left corner
currencyTxt.y = 60; // Position below wave display
// Upgrade buttons removed as requested
// Update UI elements
function updateUI() {
// Animate score if changed
if (typeof updateUI.lastScore === "undefined") updateUI.lastScore = 0;
var currentScore = LK.getScore();
if (currentScore !== updateUI.lastScore) {
scoreTxt.setText('Score: ' + currentScore);
// Animate score text pop
scoreTxt.scale.set(1.2, 1.2);
tween(scoreTxt.scale, {
x: 1,
y: 1
}, {
duration: 200,
easing: tween.easeOut
});
updateUI.lastScore = currentScore;
}
// Animate currency if changed
if (typeof updateUI.lastCurrency === "undefined") updateUI.lastCurrency = 0;
if (currency !== updateUI.lastCurrency) {
currencyTxt.setText('$: ' + currency);
currencyTxt.scale.set(1.2, 1.2);
tween(currencyTxt.scale, {
x: 1,
y: 1
}, {
duration: 200,
easing: tween.easeOut
});
updateUI.lastCurrency = currency;
}
// Animate wave if changed
if (typeof updateUI.lastWave === "undefined") updateUI.lastWave = 0;
if (wave !== updateUI.lastWave) {
waveTxt.setText('Wave: ' + wave);
waveTxt.scale.set(1.2, 1.2);
tween(waveTxt.scale, {
x: 1,
y: 1
}, {
duration: 200,
easing: tween.easeOut
});
updateUI.lastWave = wave;
}
// Update enemy counter
var aliveEnemies = 0;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] && enemies[i].active) aliveEnemies++;
}
enemyCounterTxt.setText('Enemies: ' + aliveEnemies + '/' + enemiesRequired);
// Animate enemy counter if changed
if (typeof updateUI.lastEnemies === "undefined") updateUI.lastEnemies = 0;
if (aliveEnemies !== updateUI.lastEnemies) {
enemyCounterTxt.scale.set(1.2, 1.2);
tween(enemyCounterTxt.scale, {
x: 1,
y: 1
}, {
duration: 200,
easing: tween.easeOut
});
updateUI.lastEnemies = aliveEnemies;
}
// Update next wave timer (if countdown active)
if (waveCountdown && waveCountdown.active) {
nextWaveTimerTxt.setText('Next Wave: ' + waveCountdown.countdownTime + 's');
// Animate timer if changed
if (typeof updateUI.lastWaveTimer === "undefined") updateUI.lastWaveTimer = 0;
if (waveCountdown.countdownTime !== updateUI.lastWaveTimer) {
nextWaveTimerTxt.scale.set(1.2, 1.2);
tween(nextWaveTimerTxt.scale, {
x: 1,
y: 1
}, {
duration: 200,
easing: tween.easeOut
});
updateUI.lastWaveTimer = waveCountdown.countdownTime;
}
} else {
nextWaveTimerTxt.setText('');
}
updateUpgradeButtons();
}
function updateBunkerHealth() {
var healthPercentage = Math.max(0, Math.min(100, Math.round(bunkerHealth / maxBunkerHealth * 100)));
healthTxt.setText('Bunker: ' + healthPercentage + '%');
// Animate health text color based on health
if (healthPercentage < 30) {
healthTxt.setStyle({
fill: 0xFF2222
});
} else if (healthPercentage < 60) {
healthTxt.setStyle({
fill: 0xFFFF00
});
} else {
healthTxt.setStyle({
fill: 0xFFFFFF
});
}
// Animate health text pop if health drops
if (typeof updateBunkerHealth.lastHealth === "undefined") updateBunkerHealth.lastHealth = 100;
if (healthPercentage < updateBunkerHealth.lastHealth) {
healthTxt.scale.set(1.2, 1.2);
tween(healthTxt.scale, {
x: 1,
y: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
updateBunkerHealth.lastHealth = healthPercentage;
bunker.showDamage(bunkerHealth / maxBunkerHealth);
// Only make damage indicator visible and shake when actually taking damage
if (bunkerHealth < maxBunkerHealth) {
bunker.damageIndicator.alpha = 1;
tween(bunker.damageIndicator, {
x: bunker.damageIndicator.originalX + (Math.random() * 10 - 5),
y: bunker.damageIndicator.originalY + (Math.random() * 10 - 5)
}, {
duration: 50,
repeat: 5,
yoyo: true,
onFinish: function onFinish() {
LK.setTimeout(function () {
if (gameActive) {
tween(bunker.damageIndicator, {
alpha: 0
}, {
duration: 300
});
}
}, 1000);
}
});
}
// Create a visual indicator effect when health is low
if (bunkerHealth / maxBunkerHealth < 0.5) {
LK.setTimeout(function () {
if (gameActive) bunker.showDamage(bunkerHealth / maxBunkerHealth);
}, 100);
}
}
function updateUpgradeButtons() {
// Upgrade buttons removed as requested
}
// Game mechanics functions
// Track if armored vehicle has spawned this game
if (typeof armoredVehicleSpawned === "undefined") {
var armoredVehicleSpawned = false;
}
if (typeof armoredVehicleWave === "undefined") {
var armoredVehicleWave = 0; // Track the last wave armored vehicle spawned
}
function spawnEnemy() {
var now = Date.now();
if (!waveInProgress || now - lastEnemySpawn < enemySpawnRate) return;
// Check if we've already spawned enough enemies for this wave
if (enemiesSpawned >= enemiesRequired) return;
// Limit: never spawn more than 10 alive enemies at once
var aliveEnemies = 0;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] && enemies[i].active) aliveEnemies++;
}
if (aliveEnemies >= 10) return;
lastEnemySpawn = now;
// Calculate number of enemies to spawn based on score
var currentScore = LK.getScore();
var enemiesToSpawnAtOnce = 1; // Default spawn one enemy at a time
// Increase enemies spawned at once based on more gradual score thresholds
if (currentScore >= 1200) {
enemiesToSpawnAtOnce = 5; // 5 at 1200+
} else if (currentScore >= 900) {
enemiesToSpawnAtOnce = 4; // 4 at 900+
} else if (currentScore >= 500) {
enemiesToSpawnAtOnce = 3; // 3 at 500+
} else if (currentScore >= 200) {
enemiesToSpawnAtOnce = 2; // 2 at 200+
}
// Make sure we don't spawn more enemies than required for this wave
enemiesToSpawnAtOnce = Math.min(enemiesToSpawnAtOnce, enemiesRequired - enemiesSpawned);
// Also, do not spawn more than (10 - aliveEnemies) at once
enemiesToSpawnAtOnce = Math.min(enemiesToSpawnAtOnce, 10 - aliveEnemies);
// If no room to spawn, return
if (enemiesToSpawnAtOnce <= 0) return;
// Spawn armored vehicle after 800 score, once per wave
if (currentScore >= 800 && waveInProgress && wave > 0 && armoredVehicleWave !== wave && aliveEnemies < 10) {
var armoredVehicle = new Enemy("armoredVehicle");
armoredVehicle.hp = 100;
armoredVehicle.damage = 15; // Zırhlı araç hasarı 15 olarak ayarlandı
armoredVehicle.speed = 3.5;
armoredVehicle.points = 100;
armoredVehicle.currency = 20;
// Use a random direction asset for variety
var directions = ["armoredVehicle_front", "armoredVehicle_back", "armoredVehicle_left", "armoredVehicle_right", "armoredVehicle_frontLeft", "armoredVehicle_frontRight", "armoredVehicle_backLeft", "armoredVehicle_backRight"];
var dirAsset = directions[Math.floor(Math.random() * directions.length)];
if (armoredVehicle.graphics) armoredVehicle.removeChild(armoredVehicle.graphics);
armoredVehicle.graphics = armoredVehicle.attachAsset(dirAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7
});
// Add shadow
if (armoredVehicle.shadow) armoredVehicle.removeChild(armoredVehicle.shadow);
armoredVehicle.shadow = new Shadow();
armoredVehicle.shadow.y = 40;
var baseWidth = armoredVehicle.graphics.width * armoredVehicle.graphics.scale.x;
armoredVehicle.shadow.updateSize(baseWidth);
armoredVehicle.addChildAt(armoredVehicle.shadow, 0);
armoredVehicle.shadow.x = 0;
armoredVehicle.shadow.graphics.scale.x = baseWidth * 0.8 / 100;
armoredVehicle.shadow.graphics.alpha = 0.5;
armoredVehicle.type = "armoredVehicle";
armoredVehicle.x = Math.random() * (2048 - 200) + 100;
armoredVehicle.y = -150;
enemies.push(armoredVehicle);
game.addChild(armoredVehicle);
enemiesSpawned++;
armoredVehicleWave = wave;
// Do not spawn other enemies this tick if armored vehicle is spawned
return;
}
// Spawn multiple enemies at once
for (var i = 0; i < enemiesToSpawnAtOnce; i++) {
// Determine enemy type based on score and wave progression
var enemyType = 'regular';
var random = Math.random();
// More advanced enemies appear based on score thresholds (slower, more gradual progression)
if (currentScore >= 700 && random < 0.13) {
enemyType = 'tank';
} else if (currentScore >= 500 && random < 0.11) {
enemyType = 'tank';
} else if (currentScore >= 300 && random < 0.09) {
enemyType = 'tank';
} else if (currentScore >= 200 && random < 0.18) {
enemyType = 'fast';
} else if (currentScore >= 100 && random < 0.10) {
enemyType = 'fast';
}
var enemy;
if (enemyType === 'armoredVehicle') {
// Should not happen here, armored vehicle is handled above
continue;
} else {
enemy = new Enemy(enemyType);
// Randomly assign advanced status effects for variety
if (Math.random() < 0.08) {
enemy.hasShield = true;
}
if (Math.random() < 0.05) {
enemy.burningUntil = Date.now() + 2000 + Math.random() * 2000;
enemy.burnTick = Date.now();
}
if (Math.random() < 0.05) {
enemy.poisonedUntil = Date.now() + 2000 + Math.random() * 2000;
enemy.poisonTick = Date.now();
}
if (Math.random() < 0.04) {
enemy.slowedUntil = Date.now() + 2000 + Math.random() * 2000;
enemy.slowFactor = 0.4;
}
}
// Distribute enemies across the width of the screen
if (enemiesToSpawnAtOnce > 1) {
// Distribute evenly but with some randomness
var segment = 2048 / enemiesToSpawnAtOnce;
enemy.x = i * segment + Math.random() * (segment - 100) + 50;
} else {
enemy.x = Math.random() * (2048 - 100) + 50; // Random x position
}
if (enemyType === 'desertBandit') {
enemy.y = -120; // Spawn desert bandit further back
} else {
enemy.y = -50; // Start above the screen
}
enemies.push(enemy);
game.addChild(enemy);
// Increment enemies spawned counter
enemiesSpawned++;
}
}
function checkCollisions() {
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
if (!bullet.active) {
game.removeChild(bullet);
bullets.splice(i, 1);
continue;
}
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (!enemy.active) {
game.removeChild(enemy);
enemies.splice(j, 1);
continue;
}
// Allow bullets to damage all enemies, including DesertBandit
if (bullet.active && enemy.active && bullet.intersects(enemy)) {
// Determine weaponType and crit
var weaponType = 'basic';
var isCritical = false;
if (sniper && sniper.currentWeapon) {
weaponType = sniper.currentWeapon;
// 10% crit chance for sniper, 20% for super, 5% for rifle
if (weaponType === 'sniper' && Math.random() < 0.10) isCritical = true;
if (weaponType === 'super' && Math.random() < 0.20) isCritical = true;
if (weaponType === 'basic' && Math.random() < 0.05) isCritical = true;
}
enemy.takeDamage(bullet.damage, weaponType, isCritical);
bullet.hit();
break;
}
}
}
}
function upgradeFireRate() {
if (currency >= upgrades.fireRate.cost) {
currency -= upgrades.fireRate.cost;
upgrades.fireRate.level++;
upgrades.fireRate.value += upgrades.fireRate.increment;
// Ensure fire rate doesn't go below minimum
upgrades.fireRate.value = Math.max(200, upgrades.fireRate.value);
sniper.fireRate = upgrades.fireRate.value;
// Increase cost for next upgrade
upgrades.fireRate.cost = Math.floor(upgrades.fireRate.cost * 1.5);
LK.getSound('upgrade').play();
updateUI();
}
}
function upgradeBulletDamage() {
if (currency >= upgrades.bulletDamage.cost) {
currency -= upgrades.bulletDamage.cost;
upgrades.bulletDamage.level++;
upgrades.bulletDamage.value += upgrades.bulletDamage.increment;
sniper.bulletDamage = upgrades.bulletDamage.value;
// Increase cost for next upgrade
upgrades.bulletDamage.cost = Math.floor(upgrades.bulletDamage.cost * 1.5);
LK.getSound('upgrade').play();
updateUI();
}
}
function increaseDifficulty() {
// Check if all enemies for this wave are spawned and eliminated
if (waveInProgress && enemiesSpawned >= enemiesRequired && enemies.length === 0) {
// All enemies in current wave defeated, prepare for next wave
wave++;
// Calculate new enemies required for next wave (increase by 25%)
enemiesRequired = Math.ceil(enemiesPerWave * Math.pow(1 + 0.25, wave - 1));
// Reset enemies spawned counter
enemiesSpawned = 0;
// Decrease spawn rate with each wave (faster spawns) based on score and wave
var currentScore = LK.getScore();
// Calculate spawn rate based on score: higher score = faster spawn, but never below 2200ms
// Example: every 80 score reduces spawn rate by 120ms, but never below 2200ms
var scoreSpawnReduction = Math.floor(currentScore / 80) * 120;
enemySpawnRate = Math.max(2200, 3200 - scoreSpawnReduction);
// Also apply wave-based reduction, but never below 2200ms, and reduce per wave by only 60ms
enemySpawnRate = Math.max(2200, enemySpawnRate - (wave - 1) * 60);
// Pause wave progression and show countdown
waveInProgress = false;
// Start countdown for next wave
waveCountdown.startCountdown(function () {
// When countdown completes, start the new wave
startNewWave();
});
}
}
function startNewWave() {
// Clear all existing enemies
for (var i = enemies.length - 1; i >= 0; i--) {
game.removeChild(enemies[i]);
}
enemies = [];
// Reset enemies spawned counter
enemiesSpawned = 0;
// Update UI to show new wave and enemy count
waveTxt.setText('Wave: ' + wave + ' (' + enemiesRequired + ' enemies)');
// Start spawning enemies again
waveInProgress = true;
// Flash screen to indicate new wave
LK.effects.flashScreen(0x00FF00, 500);
// Adjust difficulty based on score
var currentScore = LK.getScore();
if (currentScore > 300) {
difficulty = 5;
} else if (currentScore > 200) {
difficulty = 4;
} else if (currentScore > 100) {
difficulty = 3;
} else if (currentScore > 50) {
difficulty = 2;
} else {
difficulty = 1;
}
}
function gameOver() {
gameActive = false;
// Save high score
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
// Save currency for next game
storage.currency = currency;
// Show game over screen
LK.showGameOver();
}
// Event handlers
game.move = function (x, y, obj) {
if (gameActive) {
// If placing a wall, move it with cursor
if (game.isPlacing && game.placeableWall) {
game.placeableWall.x = x;
game.placeableWall.y = y;
} else {
// Otherwise update rifle aim to follow cursor
var angle = sniper.updateAim(x, y);
// Let the updateAim method handle the character orientation
// We don't rotate the sniper character itself anymore
// This prevents the character from being upside down when aiming
}
}
};
game.down = function (x, y, obj) {
if (!gameActive) return;
// Check if weapon shop item was clicked
if (weaponShop.checkItemClick(x, y)) {
return;
}
// Check if building shop item was clicked
if (buildingShop.checkItemClick(x, y)) {
return;
}
// If placing a wall, place it at the clicked position
if (game.isPlacing && game.placeableWall) {
// Don't place near bunker or too close to top-left corner
var distToBunker = Math.sqrt(Math.pow(x - bunker.x, 2) + Math.pow(y - bunker.y, 2));
var distToTopLeft = Math.sqrt(Math.pow(x - 100, 2) + Math.pow(y - 100, 2));
if (distToBunker < 150 || distToTopLeft < 100 || y > bunkerY - 50) {
// Can't place here - flash red
LK.effects.flashObject(game.placeableWall, 0xFF0000, 300);
return;
}
// Create an actual wall at this position
var wall = new Wall(game.placeableWall.buildingType, game.placeableWall.buildingHealth);
wall.x = x;
wall.y = y;
// --- Begin: Overlap and stacking logic for buildings ---
// If placing a sniper tower, check for overlap with other walls and adjust stacking order
if (wall.type === "sniper" && game.walls && game.walls.length > 0) {
// Find all overlapping walls (excluding self)
var overlapping = [];
for (var i = 0; i < game.walls.length; i++) {
var other = game.walls[i];
if (!other || !other.graphics) continue;
// Simple bounding box overlap check
var dx = Math.abs(wall.x - other.x);
var dy = Math.abs(wall.y - other.y);
var combinedHalfWidth = (wall.graphics.width * wall.graphics.scale.x + other.graphics.width * other.graphics.scale.x) / 2;
var combinedHalfHeight = (wall.graphics.height * wall.graphics.scale.y + other.graphics.height * other.graphics.scale.y) / 2;
if (dx < combinedHalfWidth && dy < combinedHalfHeight) {
overlapping.push(other);
}
}
// If overlap, ensure sniper tower is above if closer to player (higher y)
if (overlapping.length > 0) {
// Find the wall with the highest y (closest to player)
var topWall = wall;
for (var i = 0; i < overlapping.length; i++) {
if (overlapping[i].y > topWall.y) {
topWall = overlapping[i];
}
}
// Add new wall to game
game.addChild(wall);
// If the new wall is the top wall, bring it to the top
if (topWall === wall) {
game.setChildIndex(wall, game.children.length - 1);
} else {
// Otherwise, ensure the top wall is above
game.setChildIndex(topWall, game.children.length - 1);
}
} else {
game.addChild(wall);
}
} else if (game.walls && game.walls.length > 0) {
// If placing a non-sniper wall, check for overlap with sniper towers and other walls
var overlapping = [];
for (var i = 0; i < game.walls.length; i++) {
var other = game.walls[i];
if (!other || !other.graphics) continue;
var dx = Math.abs(wall.x - other.x);
var dy = Math.abs(wall.y - other.y);
var combinedHalfWidth = (wall.graphics.width * wall.graphics.scale.x + other.graphics.width * other.graphics.scale.x) / 2;
var combinedHalfHeight = (wall.graphics.height * wall.graphics.scale.y + other.graphics.height * other.graphics.scale.y) / 2;
if (dx < combinedHalfWidth && dy < combinedHalfHeight) {
overlapping.push(other);
}
}
if (overlapping.length > 0) {
// Find the wall with the highest y (closest to player)
var topWall = wall;
for (var i = 0; i < overlapping.length; i++) {
if (overlapping[i].y > topWall.y) {
topWall = overlapping[i];
}
}
game.addChild(wall);
if (topWall === wall) {
game.setChildIndex(wall, game.children.length - 1);
} else {
game.setChildIndex(topWall, game.children.length - 1);
}
} else {
game.addChild(wall);
}
} else {
// No other walls, just add
game.addChild(wall);
}
// --- End: Overlap and stacking logic for buildings ---
// Add to walls array if we don't have one
if (!game.walls) {
game.walls = [];
}
game.walls.push(wall);
// Remove placeable wall
game.removeChild(game.placeableWall);
game.placeableWall = null;
game.isPlacing = false;
return;
}
// Bomb placement logic
if (typeof bombMode !== "undefined" && bombMode) {
// Don't allow bomb in top left 100x100 or on UI
if (x < 100 && y < 100) {
LK.effects.flashObject(bombBtnContainer, 0xFF0000, 200);
return;
}
// Place bomb at this location
var bomb = new Bomb();
bomb.x = x;
bomb.y = y - 400; // Drop from above
game.addChild(bomb);
// Deduct money
currency -= 50;
updateUI();
// Reset bomb button state
bombMode = false;
bombBtnBg.alpha = 0.8;
bombBtn.alpha = 1;
// Cooldown: prevent spamming
bombBtnCooldown = true;
LK.setTimeout(function () {
bombBtnCooldown = false;
}, 1200);
return;
}
// Fire at touch location (single shot for all weapons)
var bullet = sniper.shoot(x, y);
if (bullet) {
bullets.push(bullet);
game.addChild(bullet);
}
};
game.up = function (x, y, obj) {
// No machine gun auto fire to stop
};
// Update function called every frame
game.update = function () {
if (!gameActive) return;
// Only spawn enemies if a wave is in progress
if (waveInProgress) {
// Spawn enemies
spawnEnemy();
// Update enemies
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].active) {
enemies[i].update();
}
}
// Check for collisions
checkCollisions();
}
// Update bullets regardless of wave status
for (var i = 0; i < bullets.length; i++) {
if (bullets[i].active) {
bullets[i].update();
}
}
// Update walls and check for wall-enemy collisions
if (game.walls && game.walls.length > 0) {
for (var i = game.walls.length - 1; i >= 0; i--) {
var wall = game.walls[i];
// Update wall health bar
wall.update();
// Check for collisions with enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (enemy.active && wall.intersects(enemy)) {
// Stop enemy and set it to attack the wall
if (!enemy.isAttackingWall) {
enemy.isAttackingWall = true;
enemy.attackTarget = wall;
enemy.attackAnimationTicks = 0;
}
// Check if wall was destroyed after the attack
if (wall.health <= 0) {
// Remove wall if destroyed
game.removeChild(wall);
game.walls.splice(i, 1);
// Reset any enemies attacking this wall
for (var k = 0; k < enemies.length; k++) {
if (enemies[k].attackTarget === wall) {
enemies[k].isAttackingWall = false;
enemies[k].attackTarget = null;
// Reset enemy position and rotation
tween(enemies[k].graphics, {
rotation: 0,
y: 0
}, {
duration: 200,
easing: tween.easeOut
});
}
}
break;
}
}
}
}
}
// Update muzzle flashes
Object.keys(sniper.muzzleFlashes).forEach(function (key) {
if (sniper.muzzleFlashes[key].active) {
sniper.muzzleFlashes[key].update();
}
});
// Update bombs
if (typeof game.children !== "undefined") {
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
// Super Ally vehicles: update and remove if inactive
if (child && child.superAlly && child.active && typeof child.update === "function") {
child.update();
if (!child.active) {
game.removeChild(child);
}
}
// Bombs
if (child && child instanceof Container && child.update && child instanceof Bomb && child.active) {
child.update();
// Remove bomb if not visible anymore
if (child.exploded && child.visible === false) {
game.removeChild(child);
}
}
}
}
// Update ally if present
if (ally && ally.active) {
ally.update();
} else if (ally && !ally.active) {
// Remove dead ally from game
game.removeChild(ally);
ally = null;
}
// Update difficulty (handles wave transitions)
increaseDifficulty();
// Continuously update damage indicator when health is low
if (bunkerHealth / maxBunkerHealth < 0.4) {
// Move damage indicator more frequently as health gets lower
if (LK.ticks % Math.max(5, Math.floor(bunkerHealth / maxBunkerHealth * 20)) === 0) {
bunker.showDamage(bunkerHealth / maxBunkerHealth);
}
}
// Update UI
if (LK.ticks % 30 === 0) {
updateUI();
}
// Update minimap every frame
updateMinimap();
};
// Make sure the weapon shop starts with the appropriate weapon equipped
weaponShop.selectWeapon(0); // Start with basic rifle selected
updateAmmoUI(); // Show initial ammo
// Initialize UI
updateUI();
// Ensure damage indicator is invisible at start
bunker.damageIndicator.alpha = 0;
updateBunkerHealth();
// Calculate initial enemies required for first wave
enemiesRequired = enemiesPerWave;
// Start the first wave with countdown
waveInProgress = false;
waveCountdown.startCountdown(function () {
startNewWave();
});
// Place random bushes and trees around the game area
function placeBushes() {
// Number of decorative elements to place
var bushCount = 25;
var treeCount = 15;
// Positions to avoid (bunker and sniper area)
var avoidX = 2048 / 2;
var avoidY = bunkerY;
var avoidRadius = 200;
// Also avoid the top-left corner where menu icon is located
var topLeftX = 50;
var topLeftY = 50;
var topLeftRadius = 100;
// Add bushes
for (var i = 0; i < bushCount; i++) {
var bush = new Bush();
// Keep generating positions until we find a suitable one
var validPosition = false;
var attempts = 0;
while (!validPosition && attempts < 10) {
// Generate random position
bush.x = Math.random() * 2048;
bush.y = Math.random() * 2732;
// Check distance from bunker area
var distToBunker = Math.sqrt(Math.pow(bush.x - avoidX, 2) + Math.pow(bush.y - avoidY, 2));
// Check distance from top-left corner
var distToTopLeft = Math.sqrt(Math.pow(bush.x - topLeftX, 2) + Math.pow(bush.y - topLeftY, 2));
// Position is valid if it's away from both areas to avoid
if (distToBunker > avoidRadius && distToTopLeft > topLeftRadius) {
validPosition = true;
}
attempts++;
}
// Add bush behind other game elements (insert at the beginning of children array)
game.addChildAt(bush, 0);
}
// Add trees
for (var i = 0; i < treeCount; i++) {
var tree = new Tree();
// Keep generating positions until we find a suitable one
var validPosition = false;
var attempts = 0;
while (!validPosition && attempts < 10) {
// Generate random position
tree.x = Math.random() * 2048;
tree.y = Math.random() * 2732;
// Check distance from bunker area
var distToBunker = Math.sqrt(Math.pow(tree.x - avoidX, 2) + Math.pow(tree.y - avoidY, 2));
// Check distance from top-left corner
var distToTopLeft = Math.sqrt(Math.pow(tree.x - topLeftX, 2) + Math.pow(tree.y - topLeftY, 2));
// Position is valid if it's away from both areas to avoid
if (distToBunker > avoidRadius && distToTopLeft > topLeftRadius) {
validPosition = true;
}
attempts++;
}
// Add tree behind other game elements (insert at the beginning of children array)
game.addChildAt(tree, 0);
}
}
// Add bushes to the game
placeBushes();
// Start background music
LK.playMusic('gameBgMusic', {
fade: {
start: 0,
end: 0.3,
duration: 1000
}
});
// Armored vehicle asset naming legend:
// front: ileri (aşağı)
// back: geri (yukarı)
// left: sola
// right: sağa
// frontLeft: çapraz aşağı-sola
// frontRight: çapraz aşağı-sağa
// backLeft: çapraz yukarı-sola
// backRight: çapraz yukarı-sağa
kaya. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
sopalı düşman adam . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
koşan ninja. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
tek gözlü dev. In-Game asset. 2d. High contrast. No shadows
barbed wire wall. In-Game asset. 2d. High contrast. No shadows
gözcü kulesi. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
yazıyı sil
Çöl haydut sniper. In-Game asset. 2d. High contrast. No shadows
önden zırhlı araç . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
zırhlı aracaın arkası. In-Game asset. 2d. High contrast. No shadows
sağa giden askeri yeşil renkte zırhlı araç. In-Game asset. 2d. High contrast. No shadows