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