/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var Goblin = Container.expand(function (pathPoints, health, speed, value) {
var self = Container.call(this);
self.pathPoints = pathPoints;
self.maxHealth = health;
self.health = health;
self.speed = speed;
self.value = value;
self.active = true;
self.currentPathIndex = 0;
self.pathProgress = 0;
var goblinGraphics = self.attachAsset('goblin', {
anchorX: 0.5,
anchorY: 0.5
});
var healthBar = LK.getAsset('tile', {
width: 60,
height: 10,
color: 0x00FF00,
anchorX: 0.5,
anchorY: 0.5
});
healthBar.y = -40;
self.addChild(healthBar);
self.x = pathPoints[0].x;
self.y = pathPoints[0].y;
self.takeDamage = function (damage) {
self.health -= damage;
// Update health bar
var healthPercent = Math.max(0, self.health / self.maxHealth);
healthBar.width = 60 * healthPercent;
healthBar.tint = healthPercent > 0.5 ? 0x00FF00 : healthPercent > 0.25 ? 0xFFFF00 : 0xFF0000;
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
self.active = false;
gold += self.value;
goldText.setText("Gold: " + gold);
score += self.value;
scoreText.setText("Score: " + score);
LK.setScore(score);
LK.getSound('goblin_death').play();
// Create coin effect
var coinText = new Text2("+" + self.value, {
size: 40,
fill: 0xFFD700
});
coinText.x = self.x;
coinText.y = self.y;
coinText.anchor.set(0.5, 0.5);
game.addChild(coinText);
tween(coinText, {
y: coinText.y - 100,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
coinText.destroy();
}
});
self.destroy();
};
self.reachedBase = function () {
self.active = false;
lives--;
livesText.setText("Lives: " + lives);
if (lives <= 0) {
endGame();
}
self.destroy();
};
self.update = function () {
if (!self.active) {
return;
}
if (self.currentPathIndex >= self.pathPoints.length - 1) {
self.reachedBase();
return;
}
var currentPoint = self.pathPoints[self.currentPathIndex];
var nextPoint = self.pathPoints[self.currentPathIndex + 1];
var dx = nextPoint.x - currentPoint.x;
var dy = nextPoint.y - currentPoint.y;
var dist = Math.sqrt(dx * dx + dy * dy);
self.pathProgress = self.currentPathIndex / (self.pathPoints.length - 1);
// Calculate how far along this segment we are
var currentX = self.x - currentPoint.x;
var currentY = self.y - currentPoint.y;
var segmentProgress = Math.sqrt(currentX * currentX + currentY * currentY) / dist;
// Add segment progress to overall progress
self.pathProgress += segmentProgress / (self.pathPoints.length - 1);
// Move towards next point
var dx = nextPoint.x - self.x;
var dy = nextPoint.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < self.speed) {
// Reached the next point
self.currentPathIndex++;
} else {
// Move along the path
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
};
return self;
});
var Projectile = Container.expand(function (source, target, damage) {
var self = Container.call(this);
self.source = source;
self.target = target;
self.damage = damage;
self.speed = 15;
self.active = true;
var projectileGraphics = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = source.x;
self.y = source.y;
self.update = function () {
if (!self.active || !self.target || !self.target.active) {
self.destroy();
return;
}
// Calculate direction vector
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 20) {
// Hit target
self.target.takeDamage(self.damage);
LK.getSound('hit').play();
self.active = false;
self.destroy();
return;
}
// Normalize and scale by speed
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
};
return self;
});
var ShieldGoblin = Container.expand(function (pathPoints, health, speed, value) {
var self = Container.call(this, pathPoints, health, speed, value);
self.maxHealth = health * 1.5; // Higher HP
self.health = self.maxHealth;
self.speed = speed * 0.8; // Slower speed
var shieldGoblinGraphics = self.attachAsset('goblin', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
var Unit = Container.expand(function (tier) {
var self = Container.call(this);
self.tier = tier || 1;
self.damage = Math.pow(2, self.tier - 1);
self.range = 300 + self.tier * 50;
self.fireRate = 1000 - self.tier * 50; // ms between shots
self.lastShot = 0;
self.targets = [];
self.isDragging = false;
self.merging = false;
var unitGraphics = self.attachAsset('unit' + self.tier, {
anchorX: 0.5,
anchorY: 0.5
});
var tierText = new Text2(self.tier.toString(), {
size: 40,
fill: 0x000000
});
tierText.anchor.set(0.5, 0.5);
self.addChild(tierText);
// Add unit name text below the level of the tower
var unitNames = ["Rock Thrower", "Slinger", "Spear Thrower", "Archer", "Fire Archer", "Crossbowman", "Musketeer", "Cannoneer", "Rifleman", "Grenadier", "Tank"];
var unitNameText = new Text2(unitNames[self.tier - 1], {
size: 30,
fill: 0x000000
});
unitNameText.anchor.set(0.5, 0);
unitNameText.y = 50; // Position below the tier text
self.addChild(unitNameText);
self.findTarget = function () {
if (self.targets.length === 0) {
return null;
}
// Sort targets by progress
self.targets.sort(function (a, b) {
return b.pathProgress - a.pathProgress;
});
// Return the most progressed goblin
return self.targets[0];
};
self.shoot = function (target) {
if (!target || !target.active) {
return;
}
var now = Date.now();
if (now - self.lastShot < self.fireRate) {
return;
}
self.lastShot = now;
var projectile = new Projectile(self, target, self.damage);
projectiles.push(projectile);
game.addChild(projectile);
LK.getSound('shoot').play();
};
self.update = function () {
if (self.merging) {
return;
}
// Clean up destroyed targets
self.targets = self.targets.filter(function (target) {
return target.active;
});
var target = self.findTarget();
if (target) {
self.shoot(target);
}
};
self.down = function (x, y, obj) {
if (!placingUnit && !gameOver) {
self.isDragging = true;
selectedUnit = self;
if (self.gridPosition) {
// Remove from grid
grid[self.gridPosition.y][self.gridPosition.x].unit = null;
self.gridPosition = null;
}
}
};
self.checkForMerge = function (gridCell) {
if (!gridCell || !gridCell.unit || gridCell.unit === self) {
return false;
}
if (gridCell.unit.tier === self.tier && self.tier < 10) {
// Merge units
mergeUnits(self, gridCell.unit);
return true;
}
return false;
};
return self;
});
var Wolf = Container.expand(function (pathPoints, health, speed, value) {
var self = Container.call(this, pathPoints, health, speed, value);
self.speed = speed * 1.5; // Faster speed
self.dodgeChance = 0.2; // 20% chance to dodge attacks
self.takeDamage = function (damage) {
if (Math.random() > self.dodgeChance) {
Goblin.prototype.takeDamage.call(self, damage);
}
};
var wolfGraphics = self.attachAsset('goblin', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x558855
});
/****
* Game Code
****/
// Game configuration
var gridWidth = 7;
var gridHeight = 6;
var tileSize = 120;
var grid = [];
var pathPoints = [];
var goblins = [];
var projectiles = [];
var units = [];
var waves = [];
var currentWave = 0;
var waveInProgress = false;
var selectedUnit = null;
var placingUnit = null;
var gold = 100;
var lives = 3;
var score = 0;
var gameOver = false;
var gridOffsetX = (2048 - gridWidth * tileSize) / 2;
var gridOffsetY = 400;
var highlight = null;
var unitCost = 35;
// UI elements
var waveText;
var goldText;
var livesText;
var scoreText;
var nextWaveButton;
var unitBuyButton;
function initializeGrid() {
for (var y = 0; y < gridHeight; y++) {
grid[y] = [];
for (var x = 0; x < gridWidth; x++) {
grid[y][x] = {
x: gridOffsetX + x * tileSize + tileSize / 2,
y: gridOffsetY + y * tileSize + tileSize / 2,
unit: null,
isPath: false
};
var tile = LK.getAsset('tile', {
anchorX: 0.5,
anchorY: 0.5
});
tile.x = grid[y][x].x;
tile.y = grid[y][x].y;
tile.alpha = 0.5;
game.addChild(tile);
}
}
}
function createPath() {
// Define a simple path through the grid
var pathCoordinates = [{
x: 0,
y: 2
}, {
x: 1,
y: 2
}, {
x: 2,
y: 2
}, {
x: 3,
y: 2
}, {
x: 3,
y: 3
}, {
x: 3,
y: 4
}, {
x: 4,
y: 4
}, {
x: 5,
y: 4
}, {
x: 6,
y: 4
}];
for (var i = 0; i < pathCoordinates.length; i++) {
var coord = pathCoordinates[i];
var cell = grid[coord.y][coord.x];
cell.isPath = true;
var pathTile = LK.getAsset('path', {
anchorX: 0.5,
anchorY: 0.5
});
pathTile.x = cell.x;
pathTile.y = cell.y;
pathTile.alpha = 0.7;
game.addChild(pathTile);
pathPoints.push({
x: cell.x,
y: cell.y
});
}
// Add the base at the end of the path
var lastCoord = pathCoordinates[pathCoordinates.length - 1];
var baseCell = grid[lastCoord.y][lastCoord.x];
var base = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5
});
base.x = baseCell.x + tileSize;
base.y = baseCell.y;
game.addChild(base);
}
function initializeWaves() {
// Define waves of increasing difficulty
waves = [{
count: 1,
// Start with 1 goblin
health: 8,
speed: 1.5,
value: 5,
delay: 1000
}];
// Adjust health and speed for each subsequent wave
for (var i = 1;; i++) {
// Generate an infinite number of waves
// Generate a large number of waves
if (i % 5 === 0) {
// Special wave every 5th wave
waves.push({
count: waves[i - 1].count + (Math.random() < 0.5 ? 1 : 2),
// Add 1 or 2 goblins randomly
health: Math.round(waves[i - 1].health * 1.02),
speed: parseFloat((waves[i - 1].speed * 1.02).toFixed(2)),
value: 10 + i,
delay: 433.33332 // Set delay to 0.43333332 seconds for special wave
});
} else {
waves.push({
count: waves[i - 1].count + (Math.random() < 0.5 ? 1 : 2),
health: Math.round(waves[i - 1].health * 1.02),
speed: parseFloat((waves[i - 1].speed * 1.02).toFixed(2)),
value: 10 + i,
delay: Math.max(200, 1000 - i * 50),
type: i >= 7 ? 'shield' : i >= 3 ? 'wolf' : 'goblin' // Add Shield Goblins from wave 7, Wolves from wave 3
});
}
}
}
function startWave() {
if (waveInProgress || gameOver) {
return;
}
waveInProgress = true;
currentWave++;
// Removed wave limit check to make waves infinite
waveText.setText("Wave: " + currentWave + " (Infinite)");
nextWaveButton.visible = false;
var wave = waves[currentWave - 1];
var goblinsReleased = 0;
var spawnInterval = LK.setInterval(function () {
spawnGoblin(wave);
goblinsReleased++;
if (goblinsReleased >= wave.count) {
LK.clearInterval(spawnInterval);
// Check every second if wave is complete
var checkInterval = LK.setInterval(function () {
if (goblins.length === 0) {
waveInProgress = false;
nextWaveButton.visible = true;
LK.clearInterval(checkInterval);
}
}, 1000);
}
}, wave.delay);
}
function spawnGoblin(wave) {
var goblin;
if (wave.type === 'shield') {
goblin = new ShieldGoblin(pathPoints, wave.health, wave.speed, wave.value);
} else if (wave.type === 'wolf') {
goblin = new Wolf(pathPoints, wave.health, wave.speed, wave.value);
} else {
goblin = new Goblin(pathPoints, wave.health, wave.speed, wave.value);
}
goblins.push(goblin);
game.addChild(goblin);
// Add this goblin as a target for all units in range
units.forEach(function (unit) {
var dx = unit.x - goblin.x;
var dy = unit.y - goblin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= unit.range) {
unit.targets.push(goblin);
}
});
}
function createUnit(tier) {
var unit = new Unit(tier);
units.push(unit);
return unit;
}
function placeUnit(unit, gridX, gridY) {
if (gridX < 0 || gridX >= gridWidth || gridY < 0 || gridY >= gridHeight) {
return false;
}
var gridCell = grid[gridY][gridX];
if (gridCell.isPath || gridCell.unit) {
return false;
}
// Place the unit
unit.x = gridCell.x;
unit.y = gridCell.y;
unit.gridPosition = {
x: gridX,
y: gridY
};
gridCell.unit = unit;
// Find nearby targets
goblins.forEach(function (goblin) {
if (goblin.active) {
var dx = unit.x - goblin.x;
var dy = unit.y - goblin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= unit.range) {
unit.targets.push(goblin);
}
}
});
LK.getSound('place').play();
return true;
}
function mergeUnits(unit1, unit2) {
// Create a new unit of the next tier
var newUnit = createUnit(unit1.tier + 1);
game.addChild(newUnit);
// Place at the position of the second unit
newUnit.gridPosition = unit2.gridPosition;
grid[unit2.gridPosition.y][unit2.gridPosition.x].unit = newUnit;
newUnit.x = unit2.x;
newUnit.y = unit2.y;
// Remove the merged units
unit1.merging = true;
unit2.merging = true;
// Effect
tween(unit1, {
x: unit2.x,
y: unit2.y,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
unit1.destroy();
removeFromArray(units, unit1);
}
});
tween(unit2, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
unit2.destroy();
removeFromArray(units, unit2);
}
});
// Scale up effect for new unit
newUnit.scale.x = 0.5;
newUnit.scale.y = 0.5;
tween(newUnit.scale, {
x: 1,
y: 1
}, {
duration: 300,
easing: tween.elasticOut
});
LK.getSound('merge').play();
return newUnit;
}
function buyUnit() {
if (gold < unitCost || placingUnit || gameOver) {
return;
}
gold -= unitCost;
goldText.setText("Gold: " + gold);
placingUnit = createUnit(1);
game.addChild(placingUnit);
// Create highlight if it doesn't exist
if (!highlight) {
highlight = LK.getAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5
});
highlight.alpha = 0.3;
game.addChild(highlight);
}
highlight.visible = true;
}
function createUI() {
// Wave text
waveText = new Text2("Wave: 0", {
size: 50,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0);
LK.gui.top.addChild(waveText);
// Gold text
goldText = new Text2("Gold: " + gold, {
size: 50,
fill: 0xFFD700
});
goldText.anchor.set(0, 0);
LK.gui.topLeft.addChild(goldText);
goldText.x = 120; // Move away from the top left corner
// Lives text
livesText = new Text2("Lives: " + lives, {
size: 50,
fill: 0xFF0000
});
livesText.anchor.set(1, 0);
LK.gui.topRight.addChild(livesText);
// Score text
scoreText = new Text2("Score: " + score, {
size: 50,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
scoreText.y = 80;
// Next wave button
nextWaveButton = new Container();
var nextWaveButtonBg = LK.getAsset('tile', {
width: 300,
height: 100,
color: 0x336699,
anchorX: 0.5,
anchorY: 0.5
});
nextWaveButton.addChild(nextWaveButtonBg);
var nextWaveButtonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF
});
nextWaveButtonText.anchor.set(0.5, 0.5);
nextWaveButton.addChild(nextWaveButtonText);
nextWaveButton.down = function () {
startWave();
};
LK.gui.bottom.addChild(nextWaveButton);
nextWaveButton.y = -150;
// Buy unit button
unitBuyButton = new Container();
var unitBuyButtonBg = LK.getAsset('tile', {
width: 300,
height: 100,
color: 0x669933,
anchorX: 0.5,
anchorY: 0.5
});
unitBuyButton.addChild(unitBuyButtonBg);
var unitBuyButtonText = new Text2("Buy Unit: " + unitCost + "g", {
size: 40,
fill: 0xFFFFFF
});
unitBuyButtonText.anchor.set(0.5, 0.5);
unitBuyButton.addChild(unitBuyButtonText);
unitBuyButton.down = function () {
buyUnit();
};
LK.gui.bottomLeft.addChild(unitBuyButton);
unitBuyButton.y = -150;
unitBuyButton.x = 170;
}
function endGame() {
if (gameOver) {
return;
}
gameOver = true;
if (score > storage.highScore) {
storage.highScore = score;
}
LK.getSound('game_over').play();
LK.showGameOver();
}
function removeFromArray(array, item) {
var index = array.indexOf(item);
if (index !== -1) {
array.splice(index, 1);
}
}
function initialize() {
initializeGrid();
createPath();
initializeWaves();
createUI();
// Play background music
LK.playMusic('bg_music');
// Create highlight for placing units (initially hidden)
highlight = LK.getAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5
});
highlight.alpha = 0.3;
highlight.visible = false;
game.addChild(highlight);
// Start with wave 0
waveText.setText("Wave: 0/" + waves.length);
}
initialize();
game.move = function (x, y) {
if (gameOver) {
return;
}
if (selectedUnit) {
selectedUnit.x = x;
selectedUnit.y = y;
} else if (placingUnit) {
// Find the closest grid cell
var gridX = Math.floor((x - gridOffsetX) / tileSize);
var gridY = Math.floor((y - gridOffsetY) / tileSize);
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
var cell = grid[gridY][gridX];
placingUnit.x = x;
placingUnit.y = y;
highlight.x = cell.x;
highlight.y = cell.y;
highlight.visible = true;
// Change highlight color based on valid placement
if (cell.isPath || cell.unit) {
highlight.tint = 0xFF0000;
} else {
highlight.tint = 0x00FF00;
}
} else {
highlight.visible = false;
}
}
};
game.down = function (x, y) {
if (gameOver) {
return;
}
// If we're placing a new unit
if (placingUnit) {
var gridX = Math.floor((x - gridOffsetX) / tileSize);
var gridY = Math.floor((y - gridOffsetY) / tileSize);
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
if (placeUnit(placingUnit, gridX, gridY)) {
placingUnit = null;
highlight.visible = false;
}
}
}
};
game.up = function (x, y) {
if (gameOver) {
return;
}
if (selectedUnit && selectedUnit.isDragging) {
var gridX = Math.floor((x - gridOffsetX) / tileSize);
var gridY = Math.floor((y - gridOffsetY) / tileSize);
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
var gridCell = grid[gridY][gridX];
if (selectedUnit.checkForMerge(gridCell)) {
// Merged successfully
} else if (!gridCell.isPath && !gridCell.unit) {
// Place on empty cell
placeUnit(selectedUnit, gridX, gridY);
} else {
// Invalid placement, return to original position if any
if (selectedUnit.gridPosition) {
selectedUnit.x = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].x;
selectedUnit.y = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].y;
grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].unit = selectedUnit;
} else {
// If it didn't have a grid position, destroy it
selectedUnit.destroy();
removeFromArray(units, selectedUnit);
}
}
} else {
// Dragged off grid, return to original position if any
if (selectedUnit.gridPosition) {
selectedUnit.x = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].x;
selectedUnit.y = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].y;
grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].unit = selectedUnit;
} else {
// If it didn't have a grid position, destroy it
selectedUnit.destroy();
removeFromArray(units, selectedUnit);
}
}
selectedUnit.isDragging = false;
selectedUnit = null;
}
};
game.update = function () {
// Update all active goblins
for (var i = goblins.length - 1; i >= 0; i--) {
if (goblins[i].active) {
goblins[i].update();
} else {
removeFromArray(goblins, goblins[i]);
}
}
// Update all active projectiles
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i].active) {
projectiles[i].update();
} else {
removeFromArray(projectiles, projectiles[i]);
}
}
// Update all units
for (var i = 0; i < units.length; i++) {
if (!units[i].merging) {
units[i].update();
}
}
// Check if units in range of goblins
units.forEach(function (unit) {
if (unit.merging) {
return;
}
// Reset targets and find new ones
unit.targets = [];
goblins.forEach(function (goblin) {
if (goblin.active) {
var dx = unit.x - goblin.x;
var dy = unit.y - goblin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= unit.range) {
unit.targets.push(goblin);
}
}
});
});
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var Goblin = Container.expand(function (pathPoints, health, speed, value) {
var self = Container.call(this);
self.pathPoints = pathPoints;
self.maxHealth = health;
self.health = health;
self.speed = speed;
self.value = value;
self.active = true;
self.currentPathIndex = 0;
self.pathProgress = 0;
var goblinGraphics = self.attachAsset('goblin', {
anchorX: 0.5,
anchorY: 0.5
});
var healthBar = LK.getAsset('tile', {
width: 60,
height: 10,
color: 0x00FF00,
anchorX: 0.5,
anchorY: 0.5
});
healthBar.y = -40;
self.addChild(healthBar);
self.x = pathPoints[0].x;
self.y = pathPoints[0].y;
self.takeDamage = function (damage) {
self.health -= damage;
// Update health bar
var healthPercent = Math.max(0, self.health / self.maxHealth);
healthBar.width = 60 * healthPercent;
healthBar.tint = healthPercent > 0.5 ? 0x00FF00 : healthPercent > 0.25 ? 0xFFFF00 : 0xFF0000;
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
self.active = false;
gold += self.value;
goldText.setText("Gold: " + gold);
score += self.value;
scoreText.setText("Score: " + score);
LK.setScore(score);
LK.getSound('goblin_death').play();
// Create coin effect
var coinText = new Text2("+" + self.value, {
size: 40,
fill: 0xFFD700
});
coinText.x = self.x;
coinText.y = self.y;
coinText.anchor.set(0.5, 0.5);
game.addChild(coinText);
tween(coinText, {
y: coinText.y - 100,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
coinText.destroy();
}
});
self.destroy();
};
self.reachedBase = function () {
self.active = false;
lives--;
livesText.setText("Lives: " + lives);
if (lives <= 0) {
endGame();
}
self.destroy();
};
self.update = function () {
if (!self.active) {
return;
}
if (self.currentPathIndex >= self.pathPoints.length - 1) {
self.reachedBase();
return;
}
var currentPoint = self.pathPoints[self.currentPathIndex];
var nextPoint = self.pathPoints[self.currentPathIndex + 1];
var dx = nextPoint.x - currentPoint.x;
var dy = nextPoint.y - currentPoint.y;
var dist = Math.sqrt(dx * dx + dy * dy);
self.pathProgress = self.currentPathIndex / (self.pathPoints.length - 1);
// Calculate how far along this segment we are
var currentX = self.x - currentPoint.x;
var currentY = self.y - currentPoint.y;
var segmentProgress = Math.sqrt(currentX * currentX + currentY * currentY) / dist;
// Add segment progress to overall progress
self.pathProgress += segmentProgress / (self.pathPoints.length - 1);
// Move towards next point
var dx = nextPoint.x - self.x;
var dy = nextPoint.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < self.speed) {
// Reached the next point
self.currentPathIndex++;
} else {
// Move along the path
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
};
return self;
});
var Projectile = Container.expand(function (source, target, damage) {
var self = Container.call(this);
self.source = source;
self.target = target;
self.damage = damage;
self.speed = 15;
self.active = true;
var projectileGraphics = self.attachAsset('projectile', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = source.x;
self.y = source.y;
self.update = function () {
if (!self.active || !self.target || !self.target.active) {
self.destroy();
return;
}
// Calculate direction vector
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 20) {
// Hit target
self.target.takeDamage(self.damage);
LK.getSound('hit').play();
self.active = false;
self.destroy();
return;
}
// Normalize and scale by speed
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
};
return self;
});
var ShieldGoblin = Container.expand(function (pathPoints, health, speed, value) {
var self = Container.call(this, pathPoints, health, speed, value);
self.maxHealth = health * 1.5; // Higher HP
self.health = self.maxHealth;
self.speed = speed * 0.8; // Slower speed
var shieldGoblinGraphics = self.attachAsset('goblin', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
var Unit = Container.expand(function (tier) {
var self = Container.call(this);
self.tier = tier || 1;
self.damage = Math.pow(2, self.tier - 1);
self.range = 300 + self.tier * 50;
self.fireRate = 1000 - self.tier * 50; // ms between shots
self.lastShot = 0;
self.targets = [];
self.isDragging = false;
self.merging = false;
var unitGraphics = self.attachAsset('unit' + self.tier, {
anchorX: 0.5,
anchorY: 0.5
});
var tierText = new Text2(self.tier.toString(), {
size: 40,
fill: 0x000000
});
tierText.anchor.set(0.5, 0.5);
self.addChild(tierText);
// Add unit name text below the level of the tower
var unitNames = ["Rock Thrower", "Slinger", "Spear Thrower", "Archer", "Fire Archer", "Crossbowman", "Musketeer", "Cannoneer", "Rifleman", "Grenadier", "Tank"];
var unitNameText = new Text2(unitNames[self.tier - 1], {
size: 30,
fill: 0x000000
});
unitNameText.anchor.set(0.5, 0);
unitNameText.y = 50; // Position below the tier text
self.addChild(unitNameText);
self.findTarget = function () {
if (self.targets.length === 0) {
return null;
}
// Sort targets by progress
self.targets.sort(function (a, b) {
return b.pathProgress - a.pathProgress;
});
// Return the most progressed goblin
return self.targets[0];
};
self.shoot = function (target) {
if (!target || !target.active) {
return;
}
var now = Date.now();
if (now - self.lastShot < self.fireRate) {
return;
}
self.lastShot = now;
var projectile = new Projectile(self, target, self.damage);
projectiles.push(projectile);
game.addChild(projectile);
LK.getSound('shoot').play();
};
self.update = function () {
if (self.merging) {
return;
}
// Clean up destroyed targets
self.targets = self.targets.filter(function (target) {
return target.active;
});
var target = self.findTarget();
if (target) {
self.shoot(target);
}
};
self.down = function (x, y, obj) {
if (!placingUnit && !gameOver) {
self.isDragging = true;
selectedUnit = self;
if (self.gridPosition) {
// Remove from grid
grid[self.gridPosition.y][self.gridPosition.x].unit = null;
self.gridPosition = null;
}
}
};
self.checkForMerge = function (gridCell) {
if (!gridCell || !gridCell.unit || gridCell.unit === self) {
return false;
}
if (gridCell.unit.tier === self.tier && self.tier < 10) {
// Merge units
mergeUnits(self, gridCell.unit);
return true;
}
return false;
};
return self;
});
var Wolf = Container.expand(function (pathPoints, health, speed, value) {
var self = Container.call(this, pathPoints, health, speed, value);
self.speed = speed * 1.5; // Faster speed
self.dodgeChance = 0.2; // 20% chance to dodge attacks
self.takeDamage = function (damage) {
if (Math.random() > self.dodgeChance) {
Goblin.prototype.takeDamage.call(self, damage);
}
};
var wolfGraphics = self.attachAsset('goblin', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x558855
});
/****
* Game Code
****/
// Game configuration
var gridWidth = 7;
var gridHeight = 6;
var tileSize = 120;
var grid = [];
var pathPoints = [];
var goblins = [];
var projectiles = [];
var units = [];
var waves = [];
var currentWave = 0;
var waveInProgress = false;
var selectedUnit = null;
var placingUnit = null;
var gold = 100;
var lives = 3;
var score = 0;
var gameOver = false;
var gridOffsetX = (2048 - gridWidth * tileSize) / 2;
var gridOffsetY = 400;
var highlight = null;
var unitCost = 35;
// UI elements
var waveText;
var goldText;
var livesText;
var scoreText;
var nextWaveButton;
var unitBuyButton;
function initializeGrid() {
for (var y = 0; y < gridHeight; y++) {
grid[y] = [];
for (var x = 0; x < gridWidth; x++) {
grid[y][x] = {
x: gridOffsetX + x * tileSize + tileSize / 2,
y: gridOffsetY + y * tileSize + tileSize / 2,
unit: null,
isPath: false
};
var tile = LK.getAsset('tile', {
anchorX: 0.5,
anchorY: 0.5
});
tile.x = grid[y][x].x;
tile.y = grid[y][x].y;
tile.alpha = 0.5;
game.addChild(tile);
}
}
}
function createPath() {
// Define a simple path through the grid
var pathCoordinates = [{
x: 0,
y: 2
}, {
x: 1,
y: 2
}, {
x: 2,
y: 2
}, {
x: 3,
y: 2
}, {
x: 3,
y: 3
}, {
x: 3,
y: 4
}, {
x: 4,
y: 4
}, {
x: 5,
y: 4
}, {
x: 6,
y: 4
}];
for (var i = 0; i < pathCoordinates.length; i++) {
var coord = pathCoordinates[i];
var cell = grid[coord.y][coord.x];
cell.isPath = true;
var pathTile = LK.getAsset('path', {
anchorX: 0.5,
anchorY: 0.5
});
pathTile.x = cell.x;
pathTile.y = cell.y;
pathTile.alpha = 0.7;
game.addChild(pathTile);
pathPoints.push({
x: cell.x,
y: cell.y
});
}
// Add the base at the end of the path
var lastCoord = pathCoordinates[pathCoordinates.length - 1];
var baseCell = grid[lastCoord.y][lastCoord.x];
var base = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5
});
base.x = baseCell.x + tileSize;
base.y = baseCell.y;
game.addChild(base);
}
function initializeWaves() {
// Define waves of increasing difficulty
waves = [{
count: 1,
// Start with 1 goblin
health: 8,
speed: 1.5,
value: 5,
delay: 1000
}];
// Adjust health and speed for each subsequent wave
for (var i = 1;; i++) {
// Generate an infinite number of waves
// Generate a large number of waves
if (i % 5 === 0) {
// Special wave every 5th wave
waves.push({
count: waves[i - 1].count + (Math.random() < 0.5 ? 1 : 2),
// Add 1 or 2 goblins randomly
health: Math.round(waves[i - 1].health * 1.02),
speed: parseFloat((waves[i - 1].speed * 1.02).toFixed(2)),
value: 10 + i,
delay: 433.33332 // Set delay to 0.43333332 seconds for special wave
});
} else {
waves.push({
count: waves[i - 1].count + (Math.random() < 0.5 ? 1 : 2),
health: Math.round(waves[i - 1].health * 1.02),
speed: parseFloat((waves[i - 1].speed * 1.02).toFixed(2)),
value: 10 + i,
delay: Math.max(200, 1000 - i * 50),
type: i >= 7 ? 'shield' : i >= 3 ? 'wolf' : 'goblin' // Add Shield Goblins from wave 7, Wolves from wave 3
});
}
}
}
function startWave() {
if (waveInProgress || gameOver) {
return;
}
waveInProgress = true;
currentWave++;
// Removed wave limit check to make waves infinite
waveText.setText("Wave: " + currentWave + " (Infinite)");
nextWaveButton.visible = false;
var wave = waves[currentWave - 1];
var goblinsReleased = 0;
var spawnInterval = LK.setInterval(function () {
spawnGoblin(wave);
goblinsReleased++;
if (goblinsReleased >= wave.count) {
LK.clearInterval(spawnInterval);
// Check every second if wave is complete
var checkInterval = LK.setInterval(function () {
if (goblins.length === 0) {
waveInProgress = false;
nextWaveButton.visible = true;
LK.clearInterval(checkInterval);
}
}, 1000);
}
}, wave.delay);
}
function spawnGoblin(wave) {
var goblin;
if (wave.type === 'shield') {
goblin = new ShieldGoblin(pathPoints, wave.health, wave.speed, wave.value);
} else if (wave.type === 'wolf') {
goblin = new Wolf(pathPoints, wave.health, wave.speed, wave.value);
} else {
goblin = new Goblin(pathPoints, wave.health, wave.speed, wave.value);
}
goblins.push(goblin);
game.addChild(goblin);
// Add this goblin as a target for all units in range
units.forEach(function (unit) {
var dx = unit.x - goblin.x;
var dy = unit.y - goblin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= unit.range) {
unit.targets.push(goblin);
}
});
}
function createUnit(tier) {
var unit = new Unit(tier);
units.push(unit);
return unit;
}
function placeUnit(unit, gridX, gridY) {
if (gridX < 0 || gridX >= gridWidth || gridY < 0 || gridY >= gridHeight) {
return false;
}
var gridCell = grid[gridY][gridX];
if (gridCell.isPath || gridCell.unit) {
return false;
}
// Place the unit
unit.x = gridCell.x;
unit.y = gridCell.y;
unit.gridPosition = {
x: gridX,
y: gridY
};
gridCell.unit = unit;
// Find nearby targets
goblins.forEach(function (goblin) {
if (goblin.active) {
var dx = unit.x - goblin.x;
var dy = unit.y - goblin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= unit.range) {
unit.targets.push(goblin);
}
}
});
LK.getSound('place').play();
return true;
}
function mergeUnits(unit1, unit2) {
// Create a new unit of the next tier
var newUnit = createUnit(unit1.tier + 1);
game.addChild(newUnit);
// Place at the position of the second unit
newUnit.gridPosition = unit2.gridPosition;
grid[unit2.gridPosition.y][unit2.gridPosition.x].unit = newUnit;
newUnit.x = unit2.x;
newUnit.y = unit2.y;
// Remove the merged units
unit1.merging = true;
unit2.merging = true;
// Effect
tween(unit1, {
x: unit2.x,
y: unit2.y,
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
unit1.destroy();
removeFromArray(units, unit1);
}
});
tween(unit2, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
unit2.destroy();
removeFromArray(units, unit2);
}
});
// Scale up effect for new unit
newUnit.scale.x = 0.5;
newUnit.scale.y = 0.5;
tween(newUnit.scale, {
x: 1,
y: 1
}, {
duration: 300,
easing: tween.elasticOut
});
LK.getSound('merge').play();
return newUnit;
}
function buyUnit() {
if (gold < unitCost || placingUnit || gameOver) {
return;
}
gold -= unitCost;
goldText.setText("Gold: " + gold);
placingUnit = createUnit(1);
game.addChild(placingUnit);
// Create highlight if it doesn't exist
if (!highlight) {
highlight = LK.getAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5
});
highlight.alpha = 0.3;
game.addChild(highlight);
}
highlight.visible = true;
}
function createUI() {
// Wave text
waveText = new Text2("Wave: 0", {
size: 50,
fill: 0xFFFFFF
});
waveText.anchor.set(0.5, 0);
LK.gui.top.addChild(waveText);
// Gold text
goldText = new Text2("Gold: " + gold, {
size: 50,
fill: 0xFFD700
});
goldText.anchor.set(0, 0);
LK.gui.topLeft.addChild(goldText);
goldText.x = 120; // Move away from the top left corner
// Lives text
livesText = new Text2("Lives: " + lives, {
size: 50,
fill: 0xFF0000
});
livesText.anchor.set(1, 0);
LK.gui.topRight.addChild(livesText);
// Score text
scoreText = new Text2("Score: " + score, {
size: 50,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
scoreText.y = 80;
// Next wave button
nextWaveButton = new Container();
var nextWaveButtonBg = LK.getAsset('tile', {
width: 300,
height: 100,
color: 0x336699,
anchorX: 0.5,
anchorY: 0.5
});
nextWaveButton.addChild(nextWaveButtonBg);
var nextWaveButtonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF
});
nextWaveButtonText.anchor.set(0.5, 0.5);
nextWaveButton.addChild(nextWaveButtonText);
nextWaveButton.down = function () {
startWave();
};
LK.gui.bottom.addChild(nextWaveButton);
nextWaveButton.y = -150;
// Buy unit button
unitBuyButton = new Container();
var unitBuyButtonBg = LK.getAsset('tile', {
width: 300,
height: 100,
color: 0x669933,
anchorX: 0.5,
anchorY: 0.5
});
unitBuyButton.addChild(unitBuyButtonBg);
var unitBuyButtonText = new Text2("Buy Unit: " + unitCost + "g", {
size: 40,
fill: 0xFFFFFF
});
unitBuyButtonText.anchor.set(0.5, 0.5);
unitBuyButton.addChild(unitBuyButtonText);
unitBuyButton.down = function () {
buyUnit();
};
LK.gui.bottomLeft.addChild(unitBuyButton);
unitBuyButton.y = -150;
unitBuyButton.x = 170;
}
function endGame() {
if (gameOver) {
return;
}
gameOver = true;
if (score > storage.highScore) {
storage.highScore = score;
}
LK.getSound('game_over').play();
LK.showGameOver();
}
function removeFromArray(array, item) {
var index = array.indexOf(item);
if (index !== -1) {
array.splice(index, 1);
}
}
function initialize() {
initializeGrid();
createPath();
initializeWaves();
createUI();
// Play background music
LK.playMusic('bg_music');
// Create highlight for placing units (initially hidden)
highlight = LK.getAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5
});
highlight.alpha = 0.3;
highlight.visible = false;
game.addChild(highlight);
// Start with wave 0
waveText.setText("Wave: 0/" + waves.length);
}
initialize();
game.move = function (x, y) {
if (gameOver) {
return;
}
if (selectedUnit) {
selectedUnit.x = x;
selectedUnit.y = y;
} else if (placingUnit) {
// Find the closest grid cell
var gridX = Math.floor((x - gridOffsetX) / tileSize);
var gridY = Math.floor((y - gridOffsetY) / tileSize);
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
var cell = grid[gridY][gridX];
placingUnit.x = x;
placingUnit.y = y;
highlight.x = cell.x;
highlight.y = cell.y;
highlight.visible = true;
// Change highlight color based on valid placement
if (cell.isPath || cell.unit) {
highlight.tint = 0xFF0000;
} else {
highlight.tint = 0x00FF00;
}
} else {
highlight.visible = false;
}
}
};
game.down = function (x, y) {
if (gameOver) {
return;
}
// If we're placing a new unit
if (placingUnit) {
var gridX = Math.floor((x - gridOffsetX) / tileSize);
var gridY = Math.floor((y - gridOffsetY) / tileSize);
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
if (placeUnit(placingUnit, gridX, gridY)) {
placingUnit = null;
highlight.visible = false;
}
}
}
};
game.up = function (x, y) {
if (gameOver) {
return;
}
if (selectedUnit && selectedUnit.isDragging) {
var gridX = Math.floor((x - gridOffsetX) / tileSize);
var gridY = Math.floor((y - gridOffsetY) / tileSize);
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
var gridCell = grid[gridY][gridX];
if (selectedUnit.checkForMerge(gridCell)) {
// Merged successfully
} else if (!gridCell.isPath && !gridCell.unit) {
// Place on empty cell
placeUnit(selectedUnit, gridX, gridY);
} else {
// Invalid placement, return to original position if any
if (selectedUnit.gridPosition) {
selectedUnit.x = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].x;
selectedUnit.y = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].y;
grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].unit = selectedUnit;
} else {
// If it didn't have a grid position, destroy it
selectedUnit.destroy();
removeFromArray(units, selectedUnit);
}
}
} else {
// Dragged off grid, return to original position if any
if (selectedUnit.gridPosition) {
selectedUnit.x = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].x;
selectedUnit.y = grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].y;
grid[selectedUnit.gridPosition.y][selectedUnit.gridPosition.x].unit = selectedUnit;
} else {
// If it didn't have a grid position, destroy it
selectedUnit.destroy();
removeFromArray(units, selectedUnit);
}
}
selectedUnit.isDragging = false;
selectedUnit = null;
}
};
game.update = function () {
// Update all active goblins
for (var i = goblins.length - 1; i >= 0; i--) {
if (goblins[i].active) {
goblins[i].update();
} else {
removeFromArray(goblins, goblins[i]);
}
}
// Update all active projectiles
for (var i = projectiles.length - 1; i >= 0; i--) {
if (projectiles[i].active) {
projectiles[i].update();
} else {
removeFromArray(projectiles, projectiles[i]);
}
}
// Update all units
for (var i = 0; i < units.length; i++) {
if (!units[i].merging) {
units[i].update();
}
}
// Check if units in range of goblins
units.forEach(function (unit) {
if (unit.merging) {
return;
}
// Reset targets and find new ones
unit.targets = [];
goblins.forEach(function (goblin) {
if (goblin.active) {
var dx = unit.x - goblin.x;
var dy = unit.y - goblin.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= unit.range) {
unit.targets.push(goblin);
}
}
});
});
};