User prompt
buat layar jadi statik ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
buat efek meledak dan flash merah saat musuh mati ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
tower mampu menyrang dari seluruh mata angin
User prompt
tower non stop menyerang saat musuh datang
User prompt
tower bisa menyerang dari 4 arah sekaligus
User prompt
perbaiki display game
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'toGlobal')' in or related to this line: 'var localPos = gameWorld.toLocal(obj.parent.toGlobal(obj.position));' Line Number: 320
Code edit (1 edits merged)
Please save this source code
User prompt
Spirit Guard: Village Defense
Initial prompt
buat game side crolling gulir ke seluruh arah. game bergenre tower defense melindungi desa dari serangan roh roh jahat
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Projectile = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('projectile', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 4; self.target = null; self.direction = null; self.damage = 20; self.update = function () { if (self.target && !self.target.destroyed) { // Move towards target var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) { self.target.takeDamage(self.damage); self.destroy(); return; } else { self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } } else if (self.direction) { // Move in fixed direction self.x += self.direction.x * self.speed; self.y += self.direction.y * self.speed; // Check if we hit any spirit along the way for (var i = 0; i < spirits.length; i++) { var spirit = spirits[i]; var dx = spirit.x - self.x; var dy = spirit.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 30) { spirit.takeDamage(self.damage); self.destroy(); return; } } // Check if we hit any tower destroyer (but they are immune) for (var i = 0; i < towerDestroyers.length; i++) { var destroyer = towerDestroyers[i]; var dx = destroyer.x - self.x; var dy = destroyer.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 30) { // Tower destroyer is immune - projectile passes through // No damage dealt, no projectile destruction } } // Destroy if projectile goes too far if (Math.abs(self.x) > 1000 || Math.abs(self.y) > 1000) { self.destroy(); return; } } else { // No target and no direction, destroy self.destroy(); return; } }; self.destroy = function () { for (var i = projectiles.length - 1; i >= 0; i--) { if (projectiles[i] === self) { projectiles.splice(i, 1); break; } } if (self.parent) { self.parent.removeChild(self); } }; return self; }); var Spirit = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('spirit', { anchorX: 0.5, anchorY: 0.5 }); self.health = 50; self.maxHealth = 50; self.speed = 1; self.gold = 10; self.targetX = villageX; self.targetY = villageY; self.hitsToKill = 4; self.hitsTaken = 0; self.update = function () { // Find the best path to village using pathfinding var bestDirection = self.findBestPath(); var dx = bestDirection.x; var dy = bestDirection.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 5) { self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } else { self.reachVillage(); } // Check collision with towers for (var i = towers.length - 1; i >= 0; i--) { var tower = towers[i]; var tdx = tower.x - self.x; var tdy = tower.y - self.y; var towerDistance = Math.sqrt(tdx * tdx + tdy * tdy); if (towerDistance < 50) { // Destroy tower towers.splice(i, 1); if (tower.parent) { tower.parent.removeChild(tower); } buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3'); // Destroy spirit too self.die(); break; } } }; self.takeDamage = function (damage) { self.hitsTaken++; LK.getSound('spirit_hit').play(); if (self.hitsTaken >= self.hitsToKill) { self.die(); } }; self.die = function () { gold += self.gold; goldText.setText('Gold: ' + gold); LK.getSound('spirit_death').play(); // Check for towers within explosion range and destroy them var explosionRange = 100; for (var i = towers.length - 1; i >= 0; i--) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= explosionRange) { // Destroy tower towers.splice(i, 1); if (tower.parent) { tower.parent.removeChild(tower); } buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3'); // Add explosion effect to destroyed tower tween(tower, { scaleX: 2, scaleY: 2, alpha: 0 }, { duration: 200, easing: tween.easeOut }); } } // Create explosion effect - scale up and fade out tween(self, { scaleX: 3, scaleY: 3, alpha: 0 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { // Remove from spirits array and destroy after explosion for (var i = spirits.length - 1; i >= 0; i--) { if (spirits[i] === self) { spirits.splice(i, 1); break; } } self.destroy(); } }); // Flash red effect tween(self, { tint: 0xFF0000 }, { duration: 100 }); tween(self, { tint: 0xFFFFFF }, { duration: 200 }); // Flash screen red LK.effects.flashScreen(0xFF0000, 200); }; self.findBestPath = function () { var directDx = self.targetX - self.x; var directDy = self.targetY - self.y; var directDistance = Math.sqrt(directDx * directDx + directDy * directDy); // Check if direct path is clear var directBlocked = false; var lookAheadDistance = 100; var checkX = self.x + directDx / directDistance * lookAheadDistance; var checkY = self.y + directDy / directDistance * lookAheadDistance; // Check for towers in direct path for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var towerDx = tower.x - checkX; var towerDy = tower.y - checkY; var towerDistance = Math.sqrt(towerDx * towerDx + towerDy * towerDy); if (towerDistance < 80) { directBlocked = true; break; } } // If direct path is clear, use it if (!directBlocked) { return { x: directDx, y: directDy }; } // Find alternative paths around obstacles var bestPath = { x: directDx, y: directDy }; var bestScore = -1000; var angles = [-90, -45, -30, -15, 15, 30, 45, 90]; // Deviation angles in degrees for (var a = 0; a < angles.length; a++) { var angleRad = angles[a] * Math.PI / 180; var newDx = directDx * Math.cos(angleRad) - directDy * Math.sin(angleRad); var newDy = directDx * Math.sin(angleRad) + directDy * Math.cos(angleRad); var newDistance = Math.sqrt(newDx * newDx + newDy * newDy); // Normalize newDx = newDx / newDistance; newDy = newDy / newDistance; // Check if this path is clear var pathClear = true; var testX = self.x + newDx * lookAheadDistance; var testY = self.y + newDy * lookAheadDistance; for (var j = 0; j < towers.length; j++) { var tower = towers[j]; var towerDx = tower.x - testX; var towerDy = tower.y - testY; var towerDistance = Math.sqrt(towerDx * towerDx + towerDy * towerDy); if (towerDistance < 90) { pathClear = false; break; } } if (pathClear) { // Score based on how close to direct path and distance to village var score = 1000 - Math.abs(angles[a]) - directDistance; if (score > bestScore) { bestScore = score; bestPath = { x: newDx * directDistance, y: newDy * directDistance }; } } } return bestPath; }; self.reachVillage = function () { villageHealth -= 10; healthText.setText('Health: ' + villageHealth); if (villageHealth <= 0) { LK.showGameOver(); } for (var i = spirits.length - 1; i >= 0; i--) { if (spirits[i] === self) { spirits.splice(i, 1); break; } } self.destroy(); }; return self; }); var Tower = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('tower', { anchorX: 0.5, anchorY: 0.5 }); self.range = 150; self.damage = 20; self.fireRate = 60; // frames between shots self.lastShot = 0; self.level = 1; self.update = function () { self.lastShot++; if (self.lastShot >= self.fireRate) { // Shoot continuously if there are any spirits on the map if (spirits.length > 0) { self.shoot(null); // Pass null since we're shooting in all directions self.lastShot = 0; } } }; self.findTarget = function () { var closestSpirit = null; var closestDistance = self.range; for (var i = 0; i < spirits.length; i++) { var spirit = spirits[i]; var dx = spirit.x - self.x; var dy = spirit.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.range && distance < closestDistance) { closestDistance = distance; closestSpirit = spirit; } } return closestSpirit; }; self.shoot = function (target) { // Fire projectiles in all 8 compass directions var directions = [{ x: 0, y: -1 }, // North { x: 0.707, y: -0.707 }, // Northeast { x: 1, y: 0 }, // East { x: 0.707, y: 0.707 }, // Southeast { x: 0, y: 1 }, // South { x: -0.707, y: 0.707 }, // Southwest { x: -1, y: 0 }, // West { x: -0.707, y: -0.707 } // Northwest ]; for (var d = 0; d < directions.length; d++) { var projectile = new Projectile(); projectile.x = self.x; projectile.y = self.y; // Always find closest spirit in this direction for targeting var dirTarget = self.findTargetInDirection(directions[d]); if (dirTarget) { projectile.target = dirTarget; } else { // Always shoot in the direction even if no specific target projectile.direction = directions[d]; } projectile.damage = self.damage; projectiles.push(projectile); gameWorld.addChild(projectile); } }; self.findTargetInDirection = function (direction) { var bestTarget = null; var bestDistance = self.range; for (var i = 0; i < spirits.length; i++) { var spirit = spirits[i]; var dx = spirit.x - self.x; var dy = spirit.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if spirit is within range if (distance <= self.range && distance < bestDistance) { // Check if spirit is roughly in the desired direction var normalizedDx = dx / distance; var normalizedDy = dy / distance; // Calculate dot product to see if spirit aligns with direction var dot = normalizedDx * direction.x + normalizedDy * direction.y; // If dot product > 0.3, spirit is in this general direction (wider angle for 8 directions) if (dot > 0.3) { bestDistance = distance; bestTarget = spirit; } } } return bestTarget; }; return self; }); var TowerDestroyer = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('towerDestroyer', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 0.8; self.gold = 20; self.isImmune = true; // Immune to weapons self.destroyedTowers = 0; self.maxTowersToDestroy = 2; self.update = function () { // Find nearest tower to destroy var nearestTower = self.findNearestTower(); if (nearestTower) { var dx = nearestTower.x - self.x; var dy = nearestTower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 5) { // Move towards tower self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } else { // Destroy tower when close enough self.destroyTower(nearestTower); } } else { // No towers left, move towards village var dx = villageX - self.x; var dy = villageY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 5) { self.x += dx / distance * self.speed; self.y += dy / distance * self.speed; } else { self.reachVillage(); } } }; self.findNearestTower = function () { var nearestTower = null; var nearestDistance = Infinity; for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var dx = tower.x - self.x; var dy = tower.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < nearestDistance) { nearestDistance = distance; nearestTower = tower; } } return nearestTower; }; self.destroyTower = function (tower) { // Remove tower from array for (var i = towers.length - 1; i >= 0; i--) { if (towers[i] === tower) { towers.splice(i, 1); break; } } // Remove tower from display if (tower.parent) { tower.parent.removeChild(tower); } // Update build button buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3'); // Add destruction effect tween(tower, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 200, easing: tween.easeOut }); // Flash screen orange LK.effects.flashScreen(0xFF4500, 300); self.destroyedTowers++; // If destroyed enough towers, disappear if (self.destroyedTowers >= self.maxTowersToDestroy) { self.die(); } }; self.takeDamage = function (damage) { // Immune to damage - do nothing return; }; self.die = function () { gold += self.gold; goldText.setText('Gold: ' + gold); // Remove from towerDestroyers array for (var i = towerDestroyers.length - 1; i >= 0; i--) { if (towerDestroyers[i] === self) { towerDestroyers.splice(i, 1); break; } } // Create disappearing effect tween(self, { scaleX: 0.1, scaleY: 0.1, alpha: 0 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; self.reachVillage = function () { villageHealth -= 15; healthText.setText('Health: ' + villageHealth); if (villageHealth <= 0) { LK.showGameOver(); } // Remove from towerDestroyers array for (var i = towerDestroyers.length - 1; i >= 0; i--) { if (towerDestroyers[i] === self) { towerDestroyers.splice(i, 1); break; } } self.destroy(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2F4F2F }); /**** * Game Code ****/ var gameWorld = new Container(); game.addChild(gameWorld); var gameWorldX = 1024; // Center horizontally (2048/2) var gameWorldY = 1366; // Center vertically (2732/2) gameWorld.x = gameWorldX; gameWorld.y = gameWorldY; // Add background var background = gameWorld.attachAsset('background', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0 }); var isDragging = false; var lastDragX = 0; var lastDragY = 0; var villageX = 0; var villageY = 0; var villageHealth = 100; var gold = 100; var wave = 1; var spiritsToSpawn = 5; var spiritsSpawned = 0; var spawnTimer = 0; var towers = []; var spirits = []; var projectiles = []; var towerDestroyers = []; var placementMode = false; var placementPreview = null; var maxTowers = 3; var towerDestroyerTimer = 0; var towerDestroyerInterval = 3600; // 60 seconds at 60 FPS // Create village at center var village = gameWorld.attachAsset('village', { anchorX: 0.5, anchorY: 0.5, x: villageX, y: villageY }); // UI Elements var goldText = new Text2('Gold: ' + gold, { size: 80, fill: 0xFFD700 }); goldText.anchor.set(1, 0); goldText.x = -20; goldText.y = 120; LK.gui.topRight.addChild(goldText); var healthText = new Text2('Health: ' + villageHealth, { size: 80, fill: 0xFF0000 }); healthText.anchor.set(1, 0); healthText.x = -20; healthText.y = 220; LK.gui.topRight.addChild(healthText); var waveText = new Text2('Wave: ' + wave, { size: 100, fill: 0xFFFFFF }); waveText.anchor.set(0.5, 0); waveText.y = 120; LK.gui.top.addChild(waveText); var buildButton = new Text2('Summon Fairy Guardian (50g) 0/3', { size: 80, fill: 0x00FF00 }); buildButton.anchor.set(0.5, 1); buildButton.y = -50; LK.gui.bottom.addChild(buildButton); var healthButton = new Text2('Health support (50g) +15HP', { size: 80, fill: 0x00FF00 }); healthButton.anchor.set(0.5, 1); healthButton.y = -150; LK.gui.bottom.addChild(healthButton); function canPlaceTower(x, y) { // Check tower count limit if (towers.length >= maxTowers) { return false; } // Check distance from village var dx = x - villageX; var dy = y - villageY; var distanceFromVillage = Math.sqrt(dx * dx + dy * dy); if (distanceFromVillage < 150) { return false; } // Check distance from other towers for (var i = 0; i < towers.length; i++) { var tower = towers[i]; var tdx = x - tower.x; var tdy = y - tower.y; var distanceFromTower = Math.sqrt(tdx * tdx + tdy * tdy); if (distanceFromTower < 80) { return false; } } return true; } function spawnSpirit() { if (spiritsSpawned >= spiritsToSpawn) { return; } var spirit = new Spirit(); // Spawn from random edge var edge = Math.floor(Math.random() * 4); var mapSize = 1500; switch (edge) { case 0: // Top spirit.x = Math.random() * mapSize - mapSize / 2; spirit.y = -mapSize / 2; break; case 1: // Right spirit.x = mapSize / 2; spirit.y = Math.random() * mapSize - mapSize / 2; break; case 2: // Bottom spirit.x = Math.random() * mapSize - mapSize / 2; spirit.y = mapSize / 2; break; case 3: // Left spirit.x = -mapSize / 2; spirit.y = Math.random() * mapSize - mapSize / 2; break; } spirits.push(spirit); gameWorld.addChild(spirit); spiritsSpawned++; } function spawnTowerDestroyer() { // Always spawn tower destroyer when called if (towers.length > 0) { var destroyer = new TowerDestroyer(); // Spawn from random edge var edge = Math.floor(Math.random() * 4); var mapSize = 1500; switch (edge) { case 0: // Top destroyer.x = Math.random() * mapSize - mapSize / 2; destroyer.y = -mapSize / 2; break; case 1: // Right destroyer.x = mapSize / 2; destroyer.y = Math.random() * mapSize - mapSize / 2; break; case 2: // Bottom destroyer.x = Math.random() * mapSize - mapSize / 2; destroyer.y = mapSize / 2; break; case 3: // Left destroyer.x = -mapSize / 2; destroyer.y = Math.random() * mapSize - mapSize / 2; break; } towerDestroyers.push(destroyer); gameWorld.addChild(destroyer); } } function nextWave() { if (spirits.length === 0 && spiritsSpawned >= spiritsToSpawn) { wave++; spiritsToSpawn = 5 + wave * 2; spiritsSpawned = 0; spawnTimer = 0; waveText.setText('Wave: ' + wave); // Bonus gold for completing wave gold += 20; goldText.setText('Gold: ' + gold); } } game.down = function (x, y, obj) { var localPos = gameWorld.toLocal(game.toGlobal({ x: x, y: y })); if (placementMode) { // Try to place tower at tapped position, or find nearest valid position var placeX = localPos.x; var placeY = localPos.y; // If exact position is invalid, try to find a nearby valid position if (!canPlaceTower(placeX, placeY)) { var found = false; var searchRadius = 100; var step = 20; // Search in expanding circles for valid placement for (var radius = step; radius <= searchRadius && !found; radius += step) { for (var angle = 0; angle < 360 && !found; angle += 45) { var testX = placeX + Math.cos(angle * Math.PI / 180) * radius; var testY = placeY + Math.sin(angle * Math.PI / 180) * radius; if (canPlaceTower(testX, testY)) { placeX = testX; placeY = testY; found = true; } } } } if (canPlaceTower(placeX, placeY) && gold >= 50) { var tower = new Tower(); tower.x = placeX; tower.y = placeY; towers.push(tower); gameWorld.addChild(tower); gold -= 50; goldText.setText('Gold: ' + gold); buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3'); buildButton.fill = 0x00FF00; // Reset to green LK.getSound('place_tower').play(); placementMode = false; if (placementPreview) { placementPreview.destroy(); placementPreview = null; } } } }; game.move = function (x, y, obj) { var localPos = gameWorld.toLocal(game.toGlobal({ x: x, y: y })); if (placementMode) { if (placementPreview) { placementPreview.destroy(); placementPreview = null; } // Always show preview, even if position is invalid var canPlace = canPlaceTower(localPos.x, localPos.y); var assetType = canPlace ? 'validPlacement' : 'invalidPlacement'; placementPreview = gameWorld.attachAsset(assetType, { anchorX: 0.5, anchorY: 0.5, x: localPos.x, y: localPos.y, alpha: 0.7 }); // Add pulsing effect to make it more responsive if (placementPreview) { tween(placementPreview, { scaleX: 1.2, scaleY: 1.2 }, { duration: 300, yoyo: true, repeat: -1 }); } } }; game.up = function (x, y, obj) { // No dragging functionality needed }; buildButton.down = function (x, y, obj) { if (gold >= 50 && towers.length < maxTowers) { placementMode = !placementMode; // Immediate visual feedback if (placementMode) { buildButton.fill = 0xFFFF00; // Yellow when in placement mode buildButton.setText('Tap to place - Cancel: tap here again'); } else { buildButton.fill = 0x00FF00; // Green when not in placement mode buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3'); } if (!placementMode && placementPreview) { placementPreview.destroy(); placementPreview = null; } } }; healthButton.down = function (x, y, obj) { if (gold >= 50 && villageHealth < 100) { gold -= 50; villageHealth += 15; // Cap health at maximum of 100 if (villageHealth > 100) { villageHealth = 100; } goldText.setText('Gold: ' + gold); healthText.setText('Health: ' + villageHealth); } }; // Start background music LK.playMusic('1battlefieldepic'); game.update = function () { // Spawn spirits spawnTimer++; if (spawnTimer >= 120) { // Spawn every 2 seconds spawnSpirit(); spawnTimer = 0; } // Spawn tower destroyer every minute towerDestroyerTimer++; if (towerDestroyerTimer >= towerDestroyerInterval) { spawnTowerDestroyer(); towerDestroyerTimer = 0; } // Check for next wave nextWave(); // Update build button color based on availability if (towers.length >= maxTowers || gold < 50) { buildButton.fill = 0xFF0000; // Red when unavailable } else { buildButton.fill = 0x00FF00; // Green when available } // Update health button color based on availability if (gold < 50 || villageHealth >= 100) { healthButton.fill = 0xFF0000; // Red when unavailable } else { healthButton.fill = 0x00FF00; // Green when available } // Update all game objects for (var i = 0; i < towers.length; i++) { towers[i].update(); } for (var i = 0; i < spirits.length; i++) { spirits[i].update(); } for (var i = 0; i < projectiles.length; i++) { projectiles[i].update(); } for (var i = 0; i < towerDestroyers.length; i++) { towerDestroyers[i].update(); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Projectile = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 4;
self.target = null;
self.direction = null;
self.damage = 20;
self.update = function () {
if (self.target && !self.target.destroyed) {
// Move towards target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 20) {
self.target.takeDamage(self.damage);
self.destroy();
return;
} else {
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
}
} else if (self.direction) {
// Move in fixed direction
self.x += self.direction.x * self.speed;
self.y += self.direction.y * self.speed;
// Check if we hit any spirit along the way
for (var i = 0; i < spirits.length; i++) {
var spirit = spirits[i];
var dx = spirit.x - self.x;
var dy = spirit.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 30) {
spirit.takeDamage(self.damage);
self.destroy();
return;
}
}
// Check if we hit any tower destroyer (but they are immune)
for (var i = 0; i < towerDestroyers.length; i++) {
var destroyer = towerDestroyers[i];
var dx = destroyer.x - self.x;
var dy = destroyer.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 30) {
// Tower destroyer is immune - projectile passes through
// No damage dealt, no projectile destruction
}
}
// Destroy if projectile goes too far
if (Math.abs(self.x) > 1000 || Math.abs(self.y) > 1000) {
self.destroy();
return;
}
} else {
// No target and no direction, destroy
self.destroy();
return;
}
};
self.destroy = function () {
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i] === self) {
projectiles.splice(i, 1);
break;
}
}
if (self.parent) {
self.parent.removeChild(self);
}
};
return self;
});
var Spirit = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('spirit', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 50;
self.maxHealth = 50;
self.speed = 1;
self.gold = 10;
self.targetX = villageX;
self.targetY = villageY;
self.hitsToKill = 4;
self.hitsTaken = 0;
self.update = function () {
// Find the best path to village using pathfinding
var bestDirection = self.findBestPath();
var dx = bestDirection.x;
var dy = bestDirection.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
} else {
self.reachVillage();
}
// Check collision with towers
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
var tdx = tower.x - self.x;
var tdy = tower.y - self.y;
var towerDistance = Math.sqrt(tdx * tdx + tdy * tdy);
if (towerDistance < 50) {
// Destroy tower
towers.splice(i, 1);
if (tower.parent) {
tower.parent.removeChild(tower);
}
buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3');
// Destroy spirit too
self.die();
break;
}
}
};
self.takeDamage = function (damage) {
self.hitsTaken++;
LK.getSound('spirit_hit').play();
if (self.hitsTaken >= self.hitsToKill) {
self.die();
}
};
self.die = function () {
gold += self.gold;
goldText.setText('Gold: ' + gold);
LK.getSound('spirit_death').play();
// Check for towers within explosion range and destroy them
var explosionRange = 100;
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= explosionRange) {
// Destroy tower
towers.splice(i, 1);
if (tower.parent) {
tower.parent.removeChild(tower);
}
buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3');
// Add explosion effect to destroyed tower
tween(tower, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut
});
}
}
// Create explosion effect - scale up and fade out
tween(self, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Remove from spirits array and destroy after explosion
for (var i = spirits.length - 1; i >= 0; i--) {
if (spirits[i] === self) {
spirits.splice(i, 1);
break;
}
}
self.destroy();
}
});
// Flash red effect
tween(self, {
tint: 0xFF0000
}, {
duration: 100
});
tween(self, {
tint: 0xFFFFFF
}, {
duration: 200
});
// Flash screen red
LK.effects.flashScreen(0xFF0000, 200);
};
self.findBestPath = function () {
var directDx = self.targetX - self.x;
var directDy = self.targetY - self.y;
var directDistance = Math.sqrt(directDx * directDx + directDy * directDy);
// Check if direct path is clear
var directBlocked = false;
var lookAheadDistance = 100;
var checkX = self.x + directDx / directDistance * lookAheadDistance;
var checkY = self.y + directDy / directDistance * lookAheadDistance;
// Check for towers in direct path
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerDx = tower.x - checkX;
var towerDy = tower.y - checkY;
var towerDistance = Math.sqrt(towerDx * towerDx + towerDy * towerDy);
if (towerDistance < 80) {
directBlocked = true;
break;
}
}
// If direct path is clear, use it
if (!directBlocked) {
return {
x: directDx,
y: directDy
};
}
// Find alternative paths around obstacles
var bestPath = {
x: directDx,
y: directDy
};
var bestScore = -1000;
var angles = [-90, -45, -30, -15, 15, 30, 45, 90]; // Deviation angles in degrees
for (var a = 0; a < angles.length; a++) {
var angleRad = angles[a] * Math.PI / 180;
var newDx = directDx * Math.cos(angleRad) - directDy * Math.sin(angleRad);
var newDy = directDx * Math.sin(angleRad) + directDy * Math.cos(angleRad);
var newDistance = Math.sqrt(newDx * newDx + newDy * newDy);
// Normalize
newDx = newDx / newDistance;
newDy = newDy / newDistance;
// Check if this path is clear
var pathClear = true;
var testX = self.x + newDx * lookAheadDistance;
var testY = self.y + newDy * lookAheadDistance;
for (var j = 0; j < towers.length; j++) {
var tower = towers[j];
var towerDx = tower.x - testX;
var towerDy = tower.y - testY;
var towerDistance = Math.sqrt(towerDx * towerDx + towerDy * towerDy);
if (towerDistance < 90) {
pathClear = false;
break;
}
}
if (pathClear) {
// Score based on how close to direct path and distance to village
var score = 1000 - Math.abs(angles[a]) - directDistance;
if (score > bestScore) {
bestScore = score;
bestPath = {
x: newDx * directDistance,
y: newDy * directDistance
};
}
}
}
return bestPath;
};
self.reachVillage = function () {
villageHealth -= 10;
healthText.setText('Health: ' + villageHealth);
if (villageHealth <= 0) {
LK.showGameOver();
}
for (var i = spirits.length - 1; i >= 0; i--) {
if (spirits[i] === self) {
spirits.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
var Tower = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
self.range = 150;
self.damage = 20;
self.fireRate = 60; // frames between shots
self.lastShot = 0;
self.level = 1;
self.update = function () {
self.lastShot++;
if (self.lastShot >= self.fireRate) {
// Shoot continuously if there are any spirits on the map
if (spirits.length > 0) {
self.shoot(null); // Pass null since we're shooting in all directions
self.lastShot = 0;
}
}
};
self.findTarget = function () {
var closestSpirit = null;
var closestDistance = self.range;
for (var i = 0; i < spirits.length; i++) {
var spirit = spirits[i];
var dx = spirit.x - self.x;
var dy = spirit.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range && distance < closestDistance) {
closestDistance = distance;
closestSpirit = spirit;
}
}
return closestSpirit;
};
self.shoot = function (target) {
// Fire projectiles in all 8 compass directions
var directions = [{
x: 0,
y: -1
},
// North
{
x: 0.707,
y: -0.707
},
// Northeast
{
x: 1,
y: 0
},
// East
{
x: 0.707,
y: 0.707
},
// Southeast
{
x: 0,
y: 1
},
// South
{
x: -0.707,
y: 0.707
},
// Southwest
{
x: -1,
y: 0
},
// West
{
x: -0.707,
y: -0.707
} // Northwest
];
for (var d = 0; d < directions.length; d++) {
var projectile = new Projectile();
projectile.x = self.x;
projectile.y = self.y;
// Always find closest spirit in this direction for targeting
var dirTarget = self.findTargetInDirection(directions[d]);
if (dirTarget) {
projectile.target = dirTarget;
} else {
// Always shoot in the direction even if no specific target
projectile.direction = directions[d];
}
projectile.damage = self.damage;
projectiles.push(projectile);
gameWorld.addChild(projectile);
}
};
self.findTargetInDirection = function (direction) {
var bestTarget = null;
var bestDistance = self.range;
for (var i = 0; i < spirits.length; i++) {
var spirit = spirits[i];
var dx = spirit.x - self.x;
var dy = spirit.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if spirit is within range
if (distance <= self.range && distance < bestDistance) {
// Check if spirit is roughly in the desired direction
var normalizedDx = dx / distance;
var normalizedDy = dy / distance;
// Calculate dot product to see if spirit aligns with direction
var dot = normalizedDx * direction.x + normalizedDy * direction.y;
// If dot product > 0.3, spirit is in this general direction (wider angle for 8 directions)
if (dot > 0.3) {
bestDistance = distance;
bestTarget = spirit;
}
}
}
return bestTarget;
};
return self;
});
var TowerDestroyer = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('towerDestroyer', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 0.8;
self.gold = 20;
self.isImmune = true; // Immune to weapons
self.destroyedTowers = 0;
self.maxTowersToDestroy = 2;
self.update = function () {
// Find nearest tower to destroy
var nearestTower = self.findNearestTower();
if (nearestTower) {
var dx = nearestTower.x - self.x;
var dy = nearestTower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Move towards tower
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
} else {
// Destroy tower when close enough
self.destroyTower(nearestTower);
}
} else {
// No towers left, move towards village
var dx = villageX - self.x;
var dy = villageY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
} else {
self.reachVillage();
}
}
};
self.findNearestTower = function () {
var nearestTower = null;
var nearestDistance = Infinity;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestTower = tower;
}
}
return nearestTower;
};
self.destroyTower = function (tower) {
// Remove tower from array
for (var i = towers.length - 1; i >= 0; i--) {
if (towers[i] === tower) {
towers.splice(i, 1);
break;
}
}
// Remove tower from display
if (tower.parent) {
tower.parent.removeChild(tower);
}
// Update build button
buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3');
// Add destruction effect
tween(tower, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut
});
// Flash screen orange
LK.effects.flashScreen(0xFF4500, 300);
self.destroyedTowers++;
// If destroyed enough towers, disappear
if (self.destroyedTowers >= self.maxTowersToDestroy) {
self.die();
}
};
self.takeDamage = function (damage) {
// Immune to damage - do nothing
return;
};
self.die = function () {
gold += self.gold;
goldText.setText('Gold: ' + gold);
// Remove from towerDestroyers array
for (var i = towerDestroyers.length - 1; i >= 0; i--) {
if (towerDestroyers[i] === self) {
towerDestroyers.splice(i, 1);
break;
}
}
// Create disappearing effect
tween(self, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
self.reachVillage = function () {
villageHealth -= 15;
healthText.setText('Health: ' + villageHealth);
if (villageHealth <= 0) {
LK.showGameOver();
}
// Remove from towerDestroyers array
for (var i = towerDestroyers.length - 1; i >= 0; i--) {
if (towerDestroyers[i] === self) {
towerDestroyers.splice(i, 1);
break;
}
}
self.destroy();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F4F2F
});
/****
* Game Code
****/
var gameWorld = new Container();
game.addChild(gameWorld);
var gameWorldX = 1024; // Center horizontally (2048/2)
var gameWorldY = 1366; // Center vertically (2732/2)
gameWorld.x = gameWorldX;
gameWorld.y = gameWorldY;
// Add background
var background = gameWorld.attachAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0
});
var isDragging = false;
var lastDragX = 0;
var lastDragY = 0;
var villageX = 0;
var villageY = 0;
var villageHealth = 100;
var gold = 100;
var wave = 1;
var spiritsToSpawn = 5;
var spiritsSpawned = 0;
var spawnTimer = 0;
var towers = [];
var spirits = [];
var projectiles = [];
var towerDestroyers = [];
var placementMode = false;
var placementPreview = null;
var maxTowers = 3;
var towerDestroyerTimer = 0;
var towerDestroyerInterval = 3600; // 60 seconds at 60 FPS
// Create village at center
var village = gameWorld.attachAsset('village', {
anchorX: 0.5,
anchorY: 0.5,
x: villageX,
y: villageY
});
// UI Elements
var goldText = new Text2('Gold: ' + gold, {
size: 80,
fill: 0xFFD700
});
goldText.anchor.set(1, 0);
goldText.x = -20;
goldText.y = 120;
LK.gui.topRight.addChild(goldText);
var healthText = new Text2('Health: ' + villageHealth, {
size: 80,
fill: 0xFF0000
});
healthText.anchor.set(1, 0);
healthText.x = -20;
healthText.y = 220;
LK.gui.topRight.addChild(healthText);
var waveText = new Text2('Wave: ' + wave, {
size: 100,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0);
waveText.y = 120;
LK.gui.top.addChild(waveText);
var buildButton = new Text2('Summon Fairy Guardian (50g) 0/3', {
size: 80,
fill: 0x00FF00
});
buildButton.anchor.set(0.5, 1);
buildButton.y = -50;
LK.gui.bottom.addChild(buildButton);
var healthButton = new Text2('Health support (50g) +15HP', {
size: 80,
fill: 0x00FF00
});
healthButton.anchor.set(0.5, 1);
healthButton.y = -150;
LK.gui.bottom.addChild(healthButton);
function canPlaceTower(x, y) {
// Check tower count limit
if (towers.length >= maxTowers) {
return false;
}
// Check distance from village
var dx = x - villageX;
var dy = y - villageY;
var distanceFromVillage = Math.sqrt(dx * dx + dy * dy);
if (distanceFromVillage < 150) {
return false;
}
// Check distance from other towers
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var tdx = x - tower.x;
var tdy = y - tower.y;
var distanceFromTower = Math.sqrt(tdx * tdx + tdy * tdy);
if (distanceFromTower < 80) {
return false;
}
}
return true;
}
function spawnSpirit() {
if (spiritsSpawned >= spiritsToSpawn) {
return;
}
var spirit = new Spirit();
// Spawn from random edge
var edge = Math.floor(Math.random() * 4);
var mapSize = 1500;
switch (edge) {
case 0:
// Top
spirit.x = Math.random() * mapSize - mapSize / 2;
spirit.y = -mapSize / 2;
break;
case 1:
// Right
spirit.x = mapSize / 2;
spirit.y = Math.random() * mapSize - mapSize / 2;
break;
case 2:
// Bottom
spirit.x = Math.random() * mapSize - mapSize / 2;
spirit.y = mapSize / 2;
break;
case 3:
// Left
spirit.x = -mapSize / 2;
spirit.y = Math.random() * mapSize - mapSize / 2;
break;
}
spirits.push(spirit);
gameWorld.addChild(spirit);
spiritsSpawned++;
}
function spawnTowerDestroyer() {
// Always spawn tower destroyer when called
if (towers.length > 0) {
var destroyer = new TowerDestroyer();
// Spawn from random edge
var edge = Math.floor(Math.random() * 4);
var mapSize = 1500;
switch (edge) {
case 0:
// Top
destroyer.x = Math.random() * mapSize - mapSize / 2;
destroyer.y = -mapSize / 2;
break;
case 1:
// Right
destroyer.x = mapSize / 2;
destroyer.y = Math.random() * mapSize - mapSize / 2;
break;
case 2:
// Bottom
destroyer.x = Math.random() * mapSize - mapSize / 2;
destroyer.y = mapSize / 2;
break;
case 3:
// Left
destroyer.x = -mapSize / 2;
destroyer.y = Math.random() * mapSize - mapSize / 2;
break;
}
towerDestroyers.push(destroyer);
gameWorld.addChild(destroyer);
}
}
function nextWave() {
if (spirits.length === 0 && spiritsSpawned >= spiritsToSpawn) {
wave++;
spiritsToSpawn = 5 + wave * 2;
spiritsSpawned = 0;
spawnTimer = 0;
waveText.setText('Wave: ' + wave);
// Bonus gold for completing wave
gold += 20;
goldText.setText('Gold: ' + gold);
}
}
game.down = function (x, y, obj) {
var localPos = gameWorld.toLocal(game.toGlobal({
x: x,
y: y
}));
if (placementMode) {
// Try to place tower at tapped position, or find nearest valid position
var placeX = localPos.x;
var placeY = localPos.y;
// If exact position is invalid, try to find a nearby valid position
if (!canPlaceTower(placeX, placeY)) {
var found = false;
var searchRadius = 100;
var step = 20;
// Search in expanding circles for valid placement
for (var radius = step; radius <= searchRadius && !found; radius += step) {
for (var angle = 0; angle < 360 && !found; angle += 45) {
var testX = placeX + Math.cos(angle * Math.PI / 180) * radius;
var testY = placeY + Math.sin(angle * Math.PI / 180) * radius;
if (canPlaceTower(testX, testY)) {
placeX = testX;
placeY = testY;
found = true;
}
}
}
}
if (canPlaceTower(placeX, placeY) && gold >= 50) {
var tower = new Tower();
tower.x = placeX;
tower.y = placeY;
towers.push(tower);
gameWorld.addChild(tower);
gold -= 50;
goldText.setText('Gold: ' + gold);
buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3');
buildButton.fill = 0x00FF00; // Reset to green
LK.getSound('place_tower').play();
placementMode = false;
if (placementPreview) {
placementPreview.destroy();
placementPreview = null;
}
}
}
};
game.move = function (x, y, obj) {
var localPos = gameWorld.toLocal(game.toGlobal({
x: x,
y: y
}));
if (placementMode) {
if (placementPreview) {
placementPreview.destroy();
placementPreview = null;
}
// Always show preview, even if position is invalid
var canPlace = canPlaceTower(localPos.x, localPos.y);
var assetType = canPlace ? 'validPlacement' : 'invalidPlacement';
placementPreview = gameWorld.attachAsset(assetType, {
anchorX: 0.5,
anchorY: 0.5,
x: localPos.x,
y: localPos.y,
alpha: 0.7
});
// Add pulsing effect to make it more responsive
if (placementPreview) {
tween(placementPreview, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
yoyo: true,
repeat: -1
});
}
}
};
game.up = function (x, y, obj) {
// No dragging functionality needed
};
buildButton.down = function (x, y, obj) {
if (gold >= 50 && towers.length < maxTowers) {
placementMode = !placementMode;
// Immediate visual feedback
if (placementMode) {
buildButton.fill = 0xFFFF00; // Yellow when in placement mode
buildButton.setText('Tap to place - Cancel: tap here again');
} else {
buildButton.fill = 0x00FF00; // Green when not in placement mode
buildButton.setText('Summon Fairy Guardian (50g) ' + towers.length + '/3');
}
if (!placementMode && placementPreview) {
placementPreview.destroy();
placementPreview = null;
}
}
};
healthButton.down = function (x, y, obj) {
if (gold >= 50 && villageHealth < 100) {
gold -= 50;
villageHealth += 15;
// Cap health at maximum of 100
if (villageHealth > 100) {
villageHealth = 100;
}
goldText.setText('Gold: ' + gold);
healthText.setText('Health: ' + villageHealth);
}
};
// Start background music
LK.playMusic('1battlefieldepic');
game.update = function () {
// Spawn spirits
spawnTimer++;
if (spawnTimer >= 120) {
// Spawn every 2 seconds
spawnSpirit();
spawnTimer = 0;
}
// Spawn tower destroyer every minute
towerDestroyerTimer++;
if (towerDestroyerTimer >= towerDestroyerInterval) {
spawnTowerDestroyer();
towerDestroyerTimer = 0;
}
// Check for next wave
nextWave();
// Update build button color based on availability
if (towers.length >= maxTowers || gold < 50) {
buildButton.fill = 0xFF0000; // Red when unavailable
} else {
buildButton.fill = 0x00FF00; // Green when available
}
// Update health button color based on availability
if (gold < 50 || villageHealth >= 100) {
healthButton.fill = 0xFF0000; // Red when unavailable
} else {
healthButton.fill = 0x00FF00; // Green when available
}
// Update all game objects
for (var i = 0; i < towers.length; i++) {
towers[i].update();
}
for (var i = 0; i < spirits.length; i++) {
spirits[i].update();
}
for (var i = 0; i < projectiles.length; i++) {
projectiles[i].update();
}
for (var i = 0; i < towerDestroyers.length; i++) {
towerDestroyers[i].update();
}
};
image top down hutan pinus luas yang ditengahnya terdapat kerajaan peri daun. 2d anime. In-Game asset. 2d. High contrast. No shadows
kesatria peri pria seluruh badan bercahaya melayang dengan tangan merengggang keatas. In-Game asset. 2d. High contrast. No shadows
roh jahat berbentuk orb gelap. In-Game asset. 2d. High contrast. No shadows
roh jahat bentuk orb api. In-Game asset. 2d. High contrast. No shadows