/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Archer = Container.expand(function () {
var self = Container.call(this);
var archerGraphics = self.attachAsset('archer', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 80;
self.maxHp = 80;
self.damage = 25;
self.moveRange = 2;
self.attackRange = 4;
self.hasActed = false;
self.hasMoved = false;
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create damage particles
for (var i = 0; i < 8; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.1,
scaleY: 0.1,
tint: 0xFF0000
});
particle.x = self.x + (Math.random() - 0.5) * 40;
particle.y = self.y + (Math.random() - 0.5) * 40;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 200;
var targetY = particle.y - Math.random() * 100 - 50;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
self.destroy();
archer = null;
checkGameOver();
}
};
self.attack = function (target) {
if (target && target.takeDamage) {
// Create arrow projectile
var arrow = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.1,
tint: 0x8B4513
});
arrow.x = self.x;
arrow.y = self.y;
game.addChild(arrow);
// Calculate angle to target
var dx = target.x - self.x;
var dy = target.y - self.y;
arrow.rotation = Math.atan2(dy, dx);
// Animate arrow to target
tween(arrow, {
x: target.x,
y: target.y
}, {
duration: 400,
easing: tween.linear,
onFinish: function onFinish() {
arrow.destroy();
target.takeDamage(self.damage);
}
});
LK.getSound('attack').play();
self.hasActed = true;
self.hasMoved = true;
}
};
self.down = function (x, y, obj) {
if (gameState === 'player_turn' && turnPhase === 'select') {
handleCellClick(self.gridX, self.gridY);
}
showHealthInterface(self);
};
return self;
});
var Barrel = Container.expand(function () {
var self = Container.call(this);
var barrelGraphics = self.attachAsset('barrel', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 30;
self.maxHp = 30;
self.exploded = false;
self.takeDamage = function (amount) {
if (self.exploded) return;
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
if (self.hp <= 0) {
self.explode();
}
};
self.explode = function () {
if (self.exploded) return;
self.exploded = true;
// Play explosion sound
LK.getSound('explosion').play();
// Create explosion particles
for (var i = 0; i < 15; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.15,
scaleY: 0.15,
tint: 0xFF4500
});
particle.x = self.x + (Math.random() - 0.5) * 60;
particle.y = self.y + (Math.random() - 0.5) * 60;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 300;
var targetY = particle.y - Math.random() * 150 - 100;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 1000 + Math.random() * 500,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
// Deal damage to all units within 2 cells radius
var damageAmount = 40;
var explosionRadius = 2;
// Check all grid positions within radius
for (var x = Math.max(0, self.gridX - explosionRadius); x <= Math.min(gridWidth - 1, self.gridX + explosionRadius); x++) {
for (var y = Math.max(0, self.gridY - explosionRadius); y <= Math.min(gridHeight - 1, self.gridY + explosionRadius); y++) {
var distance = getDistance(self.gridX, self.gridY, x, y);
if (distance <= explosionRadius && grid[x][y].occupied) {
var unit = grid[x][y].unit;
if (unit && unit.takeDamage && unit !== self) {
unit.takeDamage(damageAmount);
}
}
}
}
// Remove from grid and destroy
removeFromGrid(self.gridX, self.gridY);
for (var i = barrels.length - 1; i >= 0; i--) {
if (barrels[i] === self) {
barrels.splice(i, 1);
break;
}
}
self.destroy();
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
var ComicPanel = Container.expand(function () {
var self = Container.call(this);
self.panelImage = null;
self.titleText = null;
self.storyText = null;
self.currentPanel = 0;
self.totalPanels = 3;
self.init = function (panelNumber, title, story) {
// Create background
var background = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 2.5,
tint: 0x000000
});
background.alpha = 0.9;
self.addChild(background);
// Create panel image
self.panelImage = self.attachAsset('comicPanel' + panelNumber, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
self.panelImage.y = -100;
// Create title
self.titleText = new Text2(title, {
size: 60,
fill: 0xFFD700
});
self.titleText.anchor.set(0.5, 0.5);
self.titleText.y = 200;
self.addChild(self.titleText);
// Create story text
self.storyText = new Text2(story, {
size: 35,
fill: 0xFFFFFF
});
self.storyText.anchor.set(0.5, 0.5);
self.storyText.y = 280;
self.addChild(self.storyText);
// Create continue prompt
var continueText = new Text2('Tap to continue...', {
size: 30,
fill: 0xCCCCCC
});
continueText.anchor.set(0.5, 0.5);
continueText.y = 350;
self.addChild(continueText);
// Animate text appearance
self.titleText.alpha = 0;
self.storyText.alpha = 0;
continueText.alpha = 0;
tween(self.titleText, {
alpha: 1
}, {
duration: 500,
delay: 200
});
tween(self.storyText, {
alpha: 1
}, {
duration: 800,
delay: 600
});
tween(continueText, {
alpha: 1
}, {
duration: 500,
delay: 1200
});
};
self.down = function (x, y, obj) {
LK.getSound('pageFlip').play();
self.destroy();
game.showNextComicPanel();
};
return self;
});
var EndTurnButton = Container.expand(function () {
var self = Container.call(this);
var buttonGraphics = self.attachAsset('endTurnButton', {
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('End Turn', {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.visible = false;
self.down = function (x, y, obj) {
if (self.visible && gameState === 'player_turn') {
endPlayerTurn();
}
};
return self;
});
var GridCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.alpha = 0.3;
// Add black border effect by creating a larger black background
var borderGraphics = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.05,
scaleY: 1.05,
tint: 0x000000
});
// Move border behind main cell
self.setChildIndex(borderGraphics, 0);
self.gridX = 0;
self.gridY = 0;
self.occupied = false;
self.unit = null;
self.highlighted = false;
self.selectedBorder = null;
self.showSelectedBorder = function () {
if (!self.selectedBorder) {
self.selectedBorder = self.attachAsset('selectedBorder', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.15,
scaleY: 1.15
});
self.setChildIndex(self.selectedBorder, 0);
}
self.selectedBorder.visible = true;
};
self.hideSelectedBorder = function () {
if (self.selectedBorder) {
self.selectedBorder.visible = false;
}
};
self.highlight = function () {
if (!self.highlighted) {
cellGraphics.tint = 0xFFFF00;
// Add thick black border for highlighted cells
if (!self.highlightBorder) {
self.highlightBorder = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.1,
scaleY: 1.1,
tint: 0x000000
});
self.setChildIndex(self.highlightBorder, 0);
}
self.highlightBorder.visible = true;
self.highlighted = true;
}
};
self.unhighlight = function () {
if (self.highlighted) {
cellGraphics.tint = 0xFFFFFF;
if (self.highlightBorder) {
self.highlightBorder.visible = false;
}
self.highlighted = false;
}
};
self.down = function (x, y, obj) {
if (gameState === 'player_turn' && self.highlighted) {
handleCellClick(self.gridX, self.gridY);
}
};
return self;
});
var Knight = Container.expand(function () {
var self = Container.call(this);
var knightGraphics = self.attachAsset('knight', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 100;
self.maxHp = 100;
self.damage = 30;
self.moveRange = 3;
self.attackRange = 1;
self.hasActed = false;
self.hasMoved = false;
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create damage particles
for (var i = 0; i < 8; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.1,
scaleY: 0.1,
tint: 0xFF0000
});
particle.x = self.x + (Math.random() - 0.5) * 40;
particle.y = self.y + (Math.random() - 0.5) * 40;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 200;
var targetY = particle.y - Math.random() * 100 - 50;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
self.destroy();
knight = null;
checkGameOver();
}
};
self.attack = function (target) {
if (target && target.takeDamage) {
target.takeDamage(self.damage);
LK.getSound('attack').play();
self.hasActed = true;
self.hasMoved = true;
}
};
self.down = function (x, y, obj) {
if (gameState === 'player_turn' && turnPhase === 'select') {
handleCellClick(self.gridX, self.gridY);
}
showHealthInterface(self);
};
return self;
});
var Obstacle = Container.expand(function () {
var self = Container.call(this);
self.gridX = 0;
self.gridY = 0;
self.obstacleType = 'stone'; // 'stone', 'box', 'column'
self.isDestructible = false;
self.hp = 0;
self.maxHp = 0;
self.graphics = null;
self.init = function (type) {
self.obstacleType = type;
if (type === 'stone') {
self.graphics = self.attachAsset('stone', {
anchorX: 0.5,
anchorY: 0.5
});
self.isDestructible = false;
} else if (type === 'box') {
self.graphics = self.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5
});
self.isDestructible = true;
self.hp = 50;
self.maxHp = 50;
} else if (type === 'column') {
self.graphics = self.attachAsset('column', {
anchorX: 0.5,
anchorY: 0.5
});
self.isDestructible = false;
}
};
self.takeDamage = function (amount) {
if (!self.isDestructible) return;
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x8B4513
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = obstacles.length - 1; i >= 0; i--) {
if (obstacles[i] === self) {
obstacles.splice(i, 1);
break;
}
}
self.destroy();
}
};
self.down = function (x, y, obj) {
if (self.isDestructible) {
showHealthInterface(self);
}
};
return self;
});
var SkeletonArcher = Container.expand(function () {
var self = Container.call(this);
var archerGraphics = self.attachAsset('skeletonArcher', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 40;
self.maxHp = 40;
self.damage = 18;
self.moveRange = 3;
self.attackRange = 3;
self.attack = function (target) {
if (target && target.takeDamage) {
// Create arrow projectile
var arrow = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.1,
tint: 0x654321
});
arrow.x = self.x;
arrow.y = self.y;
game.addChild(arrow);
// Calculate angle to target
var dx = target.x - self.x;
var dy = target.y - self.y;
arrow.rotation = Math.atan2(dy, dx);
// Animate arrow to target
tween(arrow, {
x: target.x,
y: target.y
}, {
duration: 400,
easing: tween.linear,
onFinish: function onFinish() {
arrow.destroy();
target.takeDamage(self.damage);
}
});
LK.getSound('attack').play();
}
};
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create grey damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x808080
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
checkVictory();
}
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
var SkeletonKnight = Container.expand(function () {
var self = Container.call(this);
var knightGraphics = self.attachAsset('skeletonKnight', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 100;
self.maxHp = 100;
self.damage = 35;
self.moveRange = 1;
self.attackRange = 1;
self.attack = function (target) {
if (target && target.takeDamage) {
target.takeDamage(self.damage);
LK.getSound('attack').play();
}
};
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create grey damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x808080
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
checkVictory();
}
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
var SkeletonSoldier = Container.expand(function () {
var self = Container.call(this);
var soldierGraphics = self.attachAsset('skeletonSoldier', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 60;
self.maxHp = 60;
self.damage = 20;
self.moveRange = 2;
self.attackRange = 1;
self.attack = function (target) {
if (target && target.takeDamage) {
target.takeDamage(self.damage);
LK.getSound('attack').play();
}
};
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create grey damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x808080
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
checkVictory();
}
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// Comic story system
var comicStory = [{
title: "The Ancient Kingdom",
story: "Long ago, brave knights and archers defended\nthe realm from the forces of darkness..."
}, {
title: "The Skeleton Army Rises",
story: "But evil has awakened! Skeleton warriors emerge\nfrom ancient tombs to conquer the land..."
}, {
title: "Heroes Must Rise",
story: "Only you can lead our heroes to victory!\nGuide them through enemy territory to escape!"
}];
var currentComicPanel = 0;
var showingComic = true;
var comicContainer = null;
var gridWidth = 8;
var gridHeight = 8;
var cellSize = 200;
var grid = [];
var units = [];
var enemies = [];
var barrels = [];
var obstacles = [];
var knight = null;
var archer = null;
var exitPortal = null;
var selectedUnit = null;
var selectedTarget = null;
var gameState = 'player_turn'; // 'player_turn', 'enemy_turn', 'game_over'
var turnPhase = 'select'; // 'select', 'move', 'attack'
var wave = 1;
// UI Elements
var turnText = new Text2('Player Turn', {
size: 60,
fill: 0xFFFFFF
});
turnText.anchor.set(0.5, 0);
LK.gui.top.addChild(turnText);
var waveText = new Text2('Wave 1', {
size: 50,
fill: 0xFFFFFF
});
waveText.anchor.set(1, 0);
LK.gui.topRight.addChild(waveText);
var instructionText = new Text2('Select a unit to move', {
size: 40,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 1);
LK.gui.bottom.addChild(instructionText);
// Create End Turn button
var endTurnButton = new EndTurnButton();
endTurnButton.x = 2048 - 150;
endTurnButton.y = 2732 - 150;
endTurnButton.visible = false; // Hide during comic
game.addChild(endTurnButton);
// Initialize grid
function initializeGrid() {
// Optimize grid positioning for full screen - leave more space at top for UI
var startX = (2048 - gridWidth * cellSize) / 2;
var startY = 200 + (2732 - 400 - gridHeight * cellSize) / 2; // Leave 200px at top and bottom
for (var x = 0; x < gridWidth; x++) {
grid[x] = [];
for (var y = 0; y < gridHeight; y++) {
var cell = new GridCell();
cell.gridX = x;
cell.gridY = y;
cell.x = startX + x * cellSize + cellSize / 2;
cell.y = startY + y * cellSize + cellSize / 2;
grid[x][y] = cell;
game.addChild(cell);
}
}
}
function placeUnit(unit, gridX, gridY) {
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
if (grid[gridX][gridY].occupied) {
return false;
}
// Remove from old position
if (unit.gridX !== undefined && unit.gridY !== undefined) {
grid[unit.gridX][unit.gridY].occupied = false;
grid[unit.gridX][unit.gridY].unit = null;
}
unit.gridX = gridX;
unit.gridY = gridY;
unit.x = grid[gridX][gridY].x;
unit.y = grid[gridX][gridY].y;
grid[gridX][gridY].occupied = true;
grid[gridX][gridY].unit = unit;
return true;
}
return false;
}
function removeFromGrid(gridX, gridY) {
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
grid[gridX][gridY].occupied = false;
grid[gridX][gridY].unit = null;
}
}
function getDistance(x1, y1, x2, y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
function highlightValidMoves(unit) {
clearHighlights();
// Only show movement highlights if unit hasn't moved yet
if (!unit.hasMoved) {
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var distance = getDistance(unit.gridX, unit.gridY, x, y);
if (distance <= unit.moveRange && !grid[x][y].occupied) {
grid[x][y].highlight();
}
}
}
}
}
function highlightValidAttacks(unit) {
var hasValidAttacks = false;
// Only show attack highlights if unit hasn't attacked yet
if (!unit.hasActed) {
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var distance = getDistance(unit.gridX, unit.gridY, x, y);
if (distance <= unit.attackRange && grid[x][y].occupied && grid[x][y].unit !== unit) {
// Check if it's an enemy, barrel, or destructible obstacle
var isEnemy = false;
var isBarrel = false;
var isDestructibleObstacle = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === grid[x][y].unit) {
isEnemy = true;
break;
}
}
for (var i = 0; i < barrels.length; i++) {
if (barrels[i] === grid[x][y].unit) {
isBarrel = true;
break;
}
}
for (var i = 0; i < obstacles.length; i++) {
if (obstacles[i] === grid[x][y].unit && obstacles[i].isDestructible && obstacles[i].obstacleType !== 'box') {
isDestructibleObstacle = true;
break;
}
}
if (isEnemy || isBarrel || isDestructibleObstacle) {
hasValidAttacks = true;
var cell = grid[x][y];
cell.highlighted = true;
if (!cell.attackHighlight) {
cell.attackHighlight = cell.attachAsset('attackRange', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6
});
cell.setChildIndex(cell.attackHighlight, cell.children.length - 1);
}
cell.attackHighlight.visible = true;
}
}
}
}
}
return hasValidAttacks;
}
function clearHighlights() {
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var cell = grid[x][y];
cell.unhighlight();
cell.hideSelectedBorder();
if (cell.attackHighlight) {
cell.attackHighlight.visible = false;
}
}
}
}
function handleCellClick(gridX, gridY) {
if (gameState !== 'player_turn') return;
var cell = grid[gridX][gridY];
if (turnPhase === 'select') {
// Select a unit
if (cell.occupied && (cell.unit === knight || cell.unit === archer)) {
if (!cell.unit.hasActed || !cell.unit.hasMoved) {
selectedUnit = cell.unit;
// Show golden border for selected unit
grid[selectedUnit.gridX][selectedUnit.gridY].showSelectedBorder();
turnPhase = 'action';
highlightValidMoves(selectedUnit);
var hasAttacks = highlightValidAttacks(selectedUnit);
instructionText.setText('Click to move and/or attack, then click unit again when done');
}
}
} else if (turnPhase === 'action') {
if (cell.highlighted && !cell.occupied && !selectedUnit.hasMoved) {
// Move to this cell
placeUnit(selectedUnit, gridX, gridY);
selectedUnit.hasMoved = true;
// Play random movement sound
var moveSounds = ['move', 'move1', 'move3'];
var randomSound = moveSounds[Math.floor(Math.random() * moveSounds.length)];
LK.getSound(randomSound).play();
// Check if reached exit portal
if (exitPortal && gridX === exitPortal.gridX && gridY === exitPortal.gridY) {
LK.getSound('victory').play();
LK.playMusic('Victoria');
LK.showYouWin();
return;
}
// After moving, update highlights to show new attack options
clearHighlights();
highlightValidMoves(selectedUnit);
var hasAttacks = highlightValidAttacks(selectedUnit);
// If unit has no valid attacks and has moved, auto-switch to next unit
if (!hasAttacks && selectedUnit.hasMoved) {
// Find next available unit
var nextUnit = null;
if (selectedUnit === knight && archer && (!archer.hasActed || !archer.hasMoved)) {
nextUnit = archer;
} else if (selectedUnit === archer && knight && (!knight.hasActed || !knight.hasMoved)) {
nextUnit = knight;
}
if (nextUnit) {
// Mark current unit as done and switch to next
selectedUnit.hasActed = true;
grid[selectedUnit.gridX][selectedUnit.gridY].hideSelectedBorder();
selectedUnit = nextUnit;
grid[selectedUnit.gridX][selectedUnit.gridY].showSelectedBorder();
highlightValidMoves(selectedUnit);
highlightValidAttacks(selectedUnit);
instructionText.setText('Switched to next unit - Click to move and/or attack');
} else {
instructionText.setText('Click to attack or click unit again when done');
}
} else {
instructionText.setText('Click to attack or click unit again when done');
}
} else if (cell.highlighted && cell.occupied && !selectedUnit.hasActed) {
// Check if it's an enemy, barrel, or destructible obstacle
var isEnemy = false;
var isBarrel = false;
var isDestructibleObstacle = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === cell.unit) {
isEnemy = true;
break;
}
}
for (var i = 0; i < barrels.length; i++) {
if (barrels[i] === cell.unit) {
isBarrel = true;
break;
}
}
for (var i = 0; i < obstacles.length; i++) {
if (obstacles[i] === cell.unit && obstacles[i].isDestructible && obstacles[i].obstacleType !== 'box') {
isDestructibleObstacle = true;
break;
}
}
if (isEnemy || isBarrel || isDestructibleObstacle) {
// First click selects target for attack
selectedTarget = cell.unit;
turnPhase = 'confirm_attack';
clearHighlights();
// Highlight only the selected target
cell.highlight();
instructionText.setText('Click target again to confirm attack');
}
} else if (cell.occupied && cell.unit === selectedUnit) {
// Clicking on the selected unit again ends their turn
endPlayerAction();
}
} else if (turnPhase === 'confirm_attack') {
if (cell.occupied && cell.unit === selectedTarget && cell.highlighted) {
// Second click confirms attack
selectedUnit.attack(selectedTarget);
selectedUnit.hasActed = true;
selectedTarget = null;
// After attacking, unit's turn is over
clearHighlights();
endPlayerAction();
} else {
// Click elsewhere cancels attack confirmation
selectedTarget = null;
turnPhase = 'action';
highlightValidMoves(selectedUnit);
highlightValidAttacks(selectedUnit);
instructionText.setText('Click to move and/or attack, then click unit again when done');
}
}
}
function endPlayerAction() {
clearHighlights();
if (selectedUnit) {
// Mark unit as fully done (both moved and acted)
selectedUnit.hasActed = true;
selectedUnit.hasMoved = true;
grid[selectedUnit.gridX][selectedUnit.gridY].hideSelectedBorder();
}
selectedUnit = null;
turnPhase = 'select';
// End Turn button is always available during player turn
endTurnButton.visible = true;
// Check if both units have completed their full turns
var knightDone = !knight || knight.hasActed && knight.hasMoved;
var archerDone = !archer || archer.hasActed && archer.hasMoved;
if (knightDone && archerDone) {
instructionText.setText('All units ready - Click End Turn to continue');
} else {
instructionText.setText('Select next unit to move or Click End Turn to skip');
}
}
function endPlayerTurn() {
endTurnButton.visible = false;
gameState = 'enemy_turn';
turnText.setText('Enemy Turn');
instructionText.setText('Enemies moving...');
// Reset player units
if (knight) {
knight.hasActed = false;
knight.hasMoved = false;
}
if (archer) {
archer.hasActed = false;
archer.hasMoved = false;
}
// Start enemy turn after delay
LK.setTimeout(function () {
executeEnemyTurn();
}, 1000);
}
function executeEnemyTurn() {
var enemyIndex = 0;
function processNextEnemy() {
if (enemyIndex >= enemies.length) {
// All enemies processed, start new player turn
gameState = 'player_turn';
turnText.setText('Player Turn');
instructionText.setText('Select a unit to move');
turnPhase = 'select';
selectedUnit = null;
clearHighlights();
LK.getSound('trumpet').play();
return;
}
var enemy = enemies[enemyIndex];
if (!enemy || enemy.hp <= 0) {
enemyIndex++;
processNextEnemy();
return;
}
// Simple AI: move towards nearest player and attack if in range
var targets = [];
if (knight && knight.hp > 0) targets.push(knight);
if (archer && archer.hp > 0) targets.push(archer);
if (targets.length === 0) {
enemyIndex++;
processNextEnemy();
return;
}
// Find closest target
var closestTarget = targets[0];
var closestDistance = getDistance(enemy.gridX, enemy.gridY, closestTarget.gridX, closestTarget.gridY);
for (var i = 1; i < targets.length; i++) {
var distance = getDistance(enemy.gridX, enemy.gridY, targets[i].gridX, targets[i].gridY);
if (distance < closestDistance) {
closestTarget = targets[i];
closestDistance = distance;
}
}
// Try to attack first
if (closestDistance <= enemy.attackRange) {
if (enemy.attack && typeof enemy.attack === 'function') {
enemy.attack(closestTarget);
}
enemyIndex++;
LK.setTimeout(processNextEnemy, 500);
} else {
// Move towards target
var bestMove = findBestMove(enemy, closestTarget);
if (bestMove) {
// Play random movement sound
var moveSounds = ['move', 'move1', 'move3'];
var randomSound = moveSounds[Math.floor(Math.random() * moveSounds.length)];
LK.getSound(randomSound).play();
moveToPosition(enemy, bestMove.x, bestMove.y, function () {
enemyIndex++;
LK.setTimeout(processNextEnemy, 500);
});
} else {
enemyIndex++;
LK.setTimeout(processNextEnemy, 500);
}
}
}
processNextEnemy();
}
function moveToPosition(unit, targetX, targetY, callback) {
var startX = unit.gridX;
var startY = unit.gridY;
var path = [];
// Simple pathfinding - move one step at a time toward target
var currentX = startX;
var currentY = startY;
while (currentX !== targetX || currentY !== targetY) {
var nextX = currentX;
var nextY = currentY;
if (currentX < targetX) nextX++;else if (currentX > targetX) nextX--;else if (currentY < targetY) nextY++;else if (currentY > targetY) nextY--;
path.push({
x: nextX,
y: nextY
});
currentX = nextX;
currentY = nextY;
}
var stepIndex = 0;
function animateNextStep() {
if (stepIndex >= path.length) {
if (callback) callback();
return;
}
var step = path[stepIndex];
placeUnit(unit, step.x, step.y);
stepIndex++;
LK.setTimeout(animateNextStep, 300);
}
animateNextStep();
}
function findBestMove(enemy, target) {
var bestMove = null;
var bestDistance = getDistance(enemy.gridX, enemy.gridY, target.gridX, target.gridY);
for (var dx = -enemy.moveRange; dx <= enemy.moveRange; dx++) {
for (var dy = -enemy.moveRange; dy <= enemy.moveRange; dy++) {
if (Math.abs(dx) + Math.abs(dy) > enemy.moveRange) continue;
var newX = enemy.gridX + dx;
var newY = enemy.gridY + dy;
if (newX >= 0 && newX < gridWidth && newY >= 0 && newY < gridHeight) {
if (!grid[newX][newY].occupied || newX === enemy.gridX && newY === enemy.gridY) {
var distance = getDistance(newX, newY, target.gridX, target.gridY);
if (distance < bestDistance) {
bestDistance = distance;
bestMove = {
x: newX,
y: newY
};
}
}
}
}
}
return bestMove;
}
function spawnEnemies() {
var enemyTypes = [SkeletonSoldier, SkeletonArcher, SkeletonKnight];
var enemyCount = Math.min(3 + wave, 8);
for (var i = 0; i < enemyCount; i++) {
var enemyType = enemyTypes[Math.floor(Math.random() * enemyTypes.length)];
var enemy = new enemyType();
// Find random empty position on right side of map
var placed = false;
var attempts = 0;
while (!placed && attempts < 50) {
var x = Math.floor(gridWidth * 0.6 + Math.random() * gridWidth * 0.4);
var y = Math.floor(Math.random() * gridHeight);
if (!grid[x][y].occupied) {
placeUnit(enemy, x, y);
enemies.push(enemy);
game.addChild(enemy);
placed = true;
}
attempts++;
}
}
}
function spawnBarrels() {
var barrelCount = Math.min(2 + Math.floor(wave / 2), 4);
for (var i = 0; i < barrelCount; i++) {
var barrel = new Barrel();
// Find random empty position in middle area of map
var placed = false;
var attempts = 0;
while (!placed && attempts < 50) {
var x = Math.floor(gridWidth * 0.3 + Math.random() * gridWidth * 0.4);
var y = Math.floor(Math.random() * gridHeight);
if (!grid[x][y].occupied) {
placeUnit(barrel, x, y);
barrels.push(barrel);
game.addChild(barrel);
placed = true;
}
attempts++;
}
}
}
function spawnObstacles() {
var obstacleTypes = ['stone', 'box', 'column'];
var obstacleCount = Math.min(3 + Math.floor(wave / 3), 6);
for (var i = 0; i < obstacleCount; i++) {
var obstacle = new Obstacle();
var type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
obstacle.init(type);
// Find random empty position across the map
var placed = false;
var attempts = 0;
while (!placed && attempts < 50) {
var x = Math.floor(Math.random() * gridWidth);
var y = Math.floor(Math.random() * gridHeight);
// Avoid placing near starting positions and exit
if (x > 2 && x < gridWidth - 3 && !grid[x][y].occupied) {
placeUnit(obstacle, x, y);
obstacles.push(obstacle);
game.addChild(obstacle);
placed = true;
}
attempts++;
}
}
}
function checkVictory() {
if (enemies.length === 0) {
LK.getSound('trompeta').play();
LK.playMusic('Victoria');
LK.showYouWin();
return;
}
}
function showHealthInterface(unit) {
// Remove existing health interface if any
if (game.healthInterface) {
game.healthInterface.destroy();
game.healthInterface = null;
}
// Create health interface
var healthInterface = new Container();
var background = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 0.8,
tint: 0x000000
});
background.alpha = 0.8;
healthInterface.addChild(background);
// Health text
var healthText = new Text2('HP: ' + unit.hp + '/' + unit.maxHp, {
size: 40,
fill: 0xFFFFFF
});
healthText.anchor.set(0.5, 0.5);
healthInterface.addChild(healthText);
// Position interface next to end turn button, optimized for full screen
healthInterface.x = endTurnButton.x - 350;
healthInterface.y = endTurnButton.y;
game.addChild(healthInterface);
game.healthInterface = healthInterface;
// Auto-hide after 3 seconds
LK.setTimeout(function () {
if (game.healthInterface === healthInterface) {
healthInterface.destroy();
game.healthInterface = null;
}
}, 3000);
}
function checkGameOver() {
if ((!knight || knight.hp <= 0) && (!archer || archer.hp <= 0)) {
LK.showGameOver();
}
}
function showComicStory() {
if (currentComicPanel < comicStory.length) {
// Start comic music on first panel
if (currentComicPanel === 0) {
LK.playMusic('comicMusic');
}
var panel = new ComicPanel();
var storyData = comicStory[currentComicPanel];
panel.init(currentComicPanel + 1, storyData.title, storyData.story);
panel.x = 2048 / 2;
panel.y = 2732 / 2;
game.addChild(panel);
comicContainer = panel;
currentComicPanel++;
} else {
// Comic finished, start the game
startActualGame();
}
}
game.showNextComicPanel = function () {
if (comicContainer) {
comicContainer = null;
}
showComicStory();
};
function startActualGame() {
showingComic = false;
// Stop comic music and start battle music
LK.stopMusic();
LK.playMusic('battleMusic');
// Initialize the actual game
initializeGameplay();
}
function initializeGameplay() {
// Initialize grid
initializeGrid();
// Create and place heroes
knight = new Knight();
archer = new Archer();
game.addChild(knight);
game.addChild(archer);
placeUnit(knight, 1, 3);
placeUnit(archer, 1, 4);
// Create exit portal
exitPortal = LK.getAsset('exitPortal', {
anchorX: 0.5,
anchorY: 0.5
});
exitPortal.gridX = gridWidth - 2;
exitPortal.gridY = 3;
exitPortal.x = grid[exitPortal.gridX][exitPortal.gridY].x;
exitPortal.y = grid[exitPortal.gridX][exitPortal.gridY].y;
exitPortal.alpha = 0.7;
game.addChild(exitPortal);
// Spawn initial enemies
spawnEnemies();
// Spawn initial barrels
spawnBarrels();
// Spawn initial obstacles
spawnObstacles();
// Make End Turn button available from game start
endTurnButton.visible = true;
// Play trumpet sound to signal game start
LK.getSound('trumpet').play();
}
// Start with comic story
showComicStory();
game.update = function () {
// Show/hide UI based on comic state
if (showingComic) {
turnText.visible = false;
waveText.visible = false;
instructionText.visible = false;
endTurnButton.visible = false;
} else {
turnText.visible = true;
waveText.visible = true;
instructionText.visible = true;
// endTurnButton visibility handled by game logic
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Archer = Container.expand(function () {
var self = Container.call(this);
var archerGraphics = self.attachAsset('archer', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 80;
self.maxHp = 80;
self.damage = 25;
self.moveRange = 2;
self.attackRange = 4;
self.hasActed = false;
self.hasMoved = false;
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create damage particles
for (var i = 0; i < 8; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.1,
scaleY: 0.1,
tint: 0xFF0000
});
particle.x = self.x + (Math.random() - 0.5) * 40;
particle.y = self.y + (Math.random() - 0.5) * 40;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 200;
var targetY = particle.y - Math.random() * 100 - 50;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
self.destroy();
archer = null;
checkGameOver();
}
};
self.attack = function (target) {
if (target && target.takeDamage) {
// Create arrow projectile
var arrow = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.1,
tint: 0x8B4513
});
arrow.x = self.x;
arrow.y = self.y;
game.addChild(arrow);
// Calculate angle to target
var dx = target.x - self.x;
var dy = target.y - self.y;
arrow.rotation = Math.atan2(dy, dx);
// Animate arrow to target
tween(arrow, {
x: target.x,
y: target.y
}, {
duration: 400,
easing: tween.linear,
onFinish: function onFinish() {
arrow.destroy();
target.takeDamage(self.damage);
}
});
LK.getSound('attack').play();
self.hasActed = true;
self.hasMoved = true;
}
};
self.down = function (x, y, obj) {
if (gameState === 'player_turn' && turnPhase === 'select') {
handleCellClick(self.gridX, self.gridY);
}
showHealthInterface(self);
};
return self;
});
var Barrel = Container.expand(function () {
var self = Container.call(this);
var barrelGraphics = self.attachAsset('barrel', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 30;
self.maxHp = 30;
self.exploded = false;
self.takeDamage = function (amount) {
if (self.exploded) return;
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
if (self.hp <= 0) {
self.explode();
}
};
self.explode = function () {
if (self.exploded) return;
self.exploded = true;
// Play explosion sound
LK.getSound('explosion').play();
// Create explosion particles
for (var i = 0; i < 15; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.15,
scaleY: 0.15,
tint: 0xFF4500
});
particle.x = self.x + (Math.random() - 0.5) * 60;
particle.y = self.y + (Math.random() - 0.5) * 60;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 300;
var targetY = particle.y - Math.random() * 150 - 100;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 1000 + Math.random() * 500,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
// Deal damage to all units within 2 cells radius
var damageAmount = 40;
var explosionRadius = 2;
// Check all grid positions within radius
for (var x = Math.max(0, self.gridX - explosionRadius); x <= Math.min(gridWidth - 1, self.gridX + explosionRadius); x++) {
for (var y = Math.max(0, self.gridY - explosionRadius); y <= Math.min(gridHeight - 1, self.gridY + explosionRadius); y++) {
var distance = getDistance(self.gridX, self.gridY, x, y);
if (distance <= explosionRadius && grid[x][y].occupied) {
var unit = grid[x][y].unit;
if (unit && unit.takeDamage && unit !== self) {
unit.takeDamage(damageAmount);
}
}
}
}
// Remove from grid and destroy
removeFromGrid(self.gridX, self.gridY);
for (var i = barrels.length - 1; i >= 0; i--) {
if (barrels[i] === self) {
barrels.splice(i, 1);
break;
}
}
self.destroy();
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
var ComicPanel = Container.expand(function () {
var self = Container.call(this);
self.panelImage = null;
self.titleText = null;
self.storyText = null;
self.currentPanel = 0;
self.totalPanels = 3;
self.init = function (panelNumber, title, story) {
// Create background
var background = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 2.5,
tint: 0x000000
});
background.alpha = 0.9;
self.addChild(background);
// Create panel image
self.panelImage = self.attachAsset('comicPanel' + panelNumber, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
self.panelImage.y = -100;
// Create title
self.titleText = new Text2(title, {
size: 60,
fill: 0xFFD700
});
self.titleText.anchor.set(0.5, 0.5);
self.titleText.y = 200;
self.addChild(self.titleText);
// Create story text
self.storyText = new Text2(story, {
size: 35,
fill: 0xFFFFFF
});
self.storyText.anchor.set(0.5, 0.5);
self.storyText.y = 280;
self.addChild(self.storyText);
// Create continue prompt
var continueText = new Text2('Tap to continue...', {
size: 30,
fill: 0xCCCCCC
});
continueText.anchor.set(0.5, 0.5);
continueText.y = 350;
self.addChild(continueText);
// Animate text appearance
self.titleText.alpha = 0;
self.storyText.alpha = 0;
continueText.alpha = 0;
tween(self.titleText, {
alpha: 1
}, {
duration: 500,
delay: 200
});
tween(self.storyText, {
alpha: 1
}, {
duration: 800,
delay: 600
});
tween(continueText, {
alpha: 1
}, {
duration: 500,
delay: 1200
});
};
self.down = function (x, y, obj) {
LK.getSound('pageFlip').play();
self.destroy();
game.showNextComicPanel();
};
return self;
});
var EndTurnButton = Container.expand(function () {
var self = Container.call(this);
var buttonGraphics = self.attachAsset('endTurnButton', {
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('End Turn', {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.visible = false;
self.down = function (x, y, obj) {
if (self.visible && gameState === 'player_turn') {
endPlayerTurn();
}
};
return self;
});
var GridCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.alpha = 0.3;
// Add black border effect by creating a larger black background
var borderGraphics = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.05,
scaleY: 1.05,
tint: 0x000000
});
// Move border behind main cell
self.setChildIndex(borderGraphics, 0);
self.gridX = 0;
self.gridY = 0;
self.occupied = false;
self.unit = null;
self.highlighted = false;
self.selectedBorder = null;
self.showSelectedBorder = function () {
if (!self.selectedBorder) {
self.selectedBorder = self.attachAsset('selectedBorder', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.15,
scaleY: 1.15
});
self.setChildIndex(self.selectedBorder, 0);
}
self.selectedBorder.visible = true;
};
self.hideSelectedBorder = function () {
if (self.selectedBorder) {
self.selectedBorder.visible = false;
}
};
self.highlight = function () {
if (!self.highlighted) {
cellGraphics.tint = 0xFFFF00;
// Add thick black border for highlighted cells
if (!self.highlightBorder) {
self.highlightBorder = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.1,
scaleY: 1.1,
tint: 0x000000
});
self.setChildIndex(self.highlightBorder, 0);
}
self.highlightBorder.visible = true;
self.highlighted = true;
}
};
self.unhighlight = function () {
if (self.highlighted) {
cellGraphics.tint = 0xFFFFFF;
if (self.highlightBorder) {
self.highlightBorder.visible = false;
}
self.highlighted = false;
}
};
self.down = function (x, y, obj) {
if (gameState === 'player_turn' && self.highlighted) {
handleCellClick(self.gridX, self.gridY);
}
};
return self;
});
var Knight = Container.expand(function () {
var self = Container.call(this);
var knightGraphics = self.attachAsset('knight', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 100;
self.maxHp = 100;
self.damage = 30;
self.moveRange = 3;
self.attackRange = 1;
self.hasActed = false;
self.hasMoved = false;
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create damage particles
for (var i = 0; i < 8; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.1,
scaleY: 0.1,
tint: 0xFF0000
});
particle.x = self.x + (Math.random() - 0.5) * 40;
particle.y = self.y + (Math.random() - 0.5) * 40;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 200;
var targetY = particle.y - Math.random() * 100 - 50;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
self.destroy();
knight = null;
checkGameOver();
}
};
self.attack = function (target) {
if (target && target.takeDamage) {
target.takeDamage(self.damage);
LK.getSound('attack').play();
self.hasActed = true;
self.hasMoved = true;
}
};
self.down = function (x, y, obj) {
if (gameState === 'player_turn' && turnPhase === 'select') {
handleCellClick(self.gridX, self.gridY);
}
showHealthInterface(self);
};
return self;
});
var Obstacle = Container.expand(function () {
var self = Container.call(this);
self.gridX = 0;
self.gridY = 0;
self.obstacleType = 'stone'; // 'stone', 'box', 'column'
self.isDestructible = false;
self.hp = 0;
self.maxHp = 0;
self.graphics = null;
self.init = function (type) {
self.obstacleType = type;
if (type === 'stone') {
self.graphics = self.attachAsset('stone', {
anchorX: 0.5,
anchorY: 0.5
});
self.isDestructible = false;
} else if (type === 'box') {
self.graphics = self.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5
});
self.isDestructible = true;
self.hp = 50;
self.maxHp = 50;
} else if (type === 'column') {
self.graphics = self.attachAsset('column', {
anchorX: 0.5,
anchorY: 0.5
});
self.isDestructible = false;
}
};
self.takeDamage = function (amount) {
if (!self.isDestructible) return;
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x8B4513
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = obstacles.length - 1; i >= 0; i--) {
if (obstacles[i] === self) {
obstacles.splice(i, 1);
break;
}
}
self.destroy();
}
};
self.down = function (x, y, obj) {
if (self.isDestructible) {
showHealthInterface(self);
}
};
return self;
});
var SkeletonArcher = Container.expand(function () {
var self = Container.call(this);
var archerGraphics = self.attachAsset('skeletonArcher', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 40;
self.maxHp = 40;
self.damage = 18;
self.moveRange = 3;
self.attackRange = 3;
self.attack = function (target) {
if (target && target.takeDamage) {
// Create arrow projectile
var arrow = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.1,
tint: 0x654321
});
arrow.x = self.x;
arrow.y = self.y;
game.addChild(arrow);
// Calculate angle to target
var dx = target.x - self.x;
var dy = target.y - self.y;
arrow.rotation = Math.atan2(dy, dx);
// Animate arrow to target
tween(arrow, {
x: target.x,
y: target.y
}, {
duration: 400,
easing: tween.linear,
onFinish: function onFinish() {
arrow.destroy();
target.takeDamage(self.damage);
}
});
LK.getSound('attack').play();
}
};
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create grey damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x808080
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
checkVictory();
}
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
var SkeletonKnight = Container.expand(function () {
var self = Container.call(this);
var knightGraphics = self.attachAsset('skeletonKnight', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 100;
self.maxHp = 100;
self.damage = 35;
self.moveRange = 1;
self.attackRange = 1;
self.attack = function (target) {
if (target && target.takeDamage) {
target.takeDamage(self.damage);
LK.getSound('attack').play();
}
};
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create grey damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x808080
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
checkVictory();
}
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
var SkeletonSoldier = Container.expand(function () {
var self = Container.call(this);
var soldierGraphics = self.attachAsset('skeletonSoldier', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 0;
self.gridY = 0;
self.hp = 60;
self.maxHp = 60;
self.damage = 20;
self.moveRange = 2;
self.attackRange = 1;
self.attack = function (target) {
if (target && target.takeDamage) {
target.takeDamage(self.damage);
LK.getSound('attack').play();
}
};
self.takeDamage = function (amount) {
self.hp = Math.max(0, self.hp - amount);
LK.effects.flashObject(self, 0xFF0000, 500);
// Create grey damage particles
for (var i = 0; i < 6; i++) {
var particle = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.08,
scaleY: 0.08,
tint: 0x808080
});
particle.x = self.x + (Math.random() - 0.5) * 30;
particle.y = self.y + (Math.random() - 0.5) * 30;
game.addChild(particle);
var targetX = particle.x + (Math.random() - 0.5) * 150;
var targetY = particle.y - Math.random() * 80 - 40;
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.03,
scaleY: 0.03
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
if (self.hp <= 0) {
removeFromGrid(self.gridX, self.gridY);
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
self.destroy();
checkVictory();
}
};
self.down = function (x, y, obj) {
showHealthInterface(self);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// Comic story system
var comicStory = [{
title: "The Ancient Kingdom",
story: "Long ago, brave knights and archers defended\nthe realm from the forces of darkness..."
}, {
title: "The Skeleton Army Rises",
story: "But evil has awakened! Skeleton warriors emerge\nfrom ancient tombs to conquer the land..."
}, {
title: "Heroes Must Rise",
story: "Only you can lead our heroes to victory!\nGuide them through enemy territory to escape!"
}];
var currentComicPanel = 0;
var showingComic = true;
var comicContainer = null;
var gridWidth = 8;
var gridHeight = 8;
var cellSize = 200;
var grid = [];
var units = [];
var enemies = [];
var barrels = [];
var obstacles = [];
var knight = null;
var archer = null;
var exitPortal = null;
var selectedUnit = null;
var selectedTarget = null;
var gameState = 'player_turn'; // 'player_turn', 'enemy_turn', 'game_over'
var turnPhase = 'select'; // 'select', 'move', 'attack'
var wave = 1;
// UI Elements
var turnText = new Text2('Player Turn', {
size: 60,
fill: 0xFFFFFF
});
turnText.anchor.set(0.5, 0);
LK.gui.top.addChild(turnText);
var waveText = new Text2('Wave 1', {
size: 50,
fill: 0xFFFFFF
});
waveText.anchor.set(1, 0);
LK.gui.topRight.addChild(waveText);
var instructionText = new Text2('Select a unit to move', {
size: 40,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 1);
LK.gui.bottom.addChild(instructionText);
// Create End Turn button
var endTurnButton = new EndTurnButton();
endTurnButton.x = 2048 - 150;
endTurnButton.y = 2732 - 150;
endTurnButton.visible = false; // Hide during comic
game.addChild(endTurnButton);
// Initialize grid
function initializeGrid() {
// Optimize grid positioning for full screen - leave more space at top for UI
var startX = (2048 - gridWidth * cellSize) / 2;
var startY = 200 + (2732 - 400 - gridHeight * cellSize) / 2; // Leave 200px at top and bottom
for (var x = 0; x < gridWidth; x++) {
grid[x] = [];
for (var y = 0; y < gridHeight; y++) {
var cell = new GridCell();
cell.gridX = x;
cell.gridY = y;
cell.x = startX + x * cellSize + cellSize / 2;
cell.y = startY + y * cellSize + cellSize / 2;
grid[x][y] = cell;
game.addChild(cell);
}
}
}
function placeUnit(unit, gridX, gridY) {
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
if (grid[gridX][gridY].occupied) {
return false;
}
// Remove from old position
if (unit.gridX !== undefined && unit.gridY !== undefined) {
grid[unit.gridX][unit.gridY].occupied = false;
grid[unit.gridX][unit.gridY].unit = null;
}
unit.gridX = gridX;
unit.gridY = gridY;
unit.x = grid[gridX][gridY].x;
unit.y = grid[gridX][gridY].y;
grid[gridX][gridY].occupied = true;
grid[gridX][gridY].unit = unit;
return true;
}
return false;
}
function removeFromGrid(gridX, gridY) {
if (gridX >= 0 && gridX < gridWidth && gridY >= 0 && gridY < gridHeight) {
grid[gridX][gridY].occupied = false;
grid[gridX][gridY].unit = null;
}
}
function getDistance(x1, y1, x2, y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
function highlightValidMoves(unit) {
clearHighlights();
// Only show movement highlights if unit hasn't moved yet
if (!unit.hasMoved) {
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var distance = getDistance(unit.gridX, unit.gridY, x, y);
if (distance <= unit.moveRange && !grid[x][y].occupied) {
grid[x][y].highlight();
}
}
}
}
}
function highlightValidAttacks(unit) {
var hasValidAttacks = false;
// Only show attack highlights if unit hasn't attacked yet
if (!unit.hasActed) {
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var distance = getDistance(unit.gridX, unit.gridY, x, y);
if (distance <= unit.attackRange && grid[x][y].occupied && grid[x][y].unit !== unit) {
// Check if it's an enemy, barrel, or destructible obstacle
var isEnemy = false;
var isBarrel = false;
var isDestructibleObstacle = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === grid[x][y].unit) {
isEnemy = true;
break;
}
}
for (var i = 0; i < barrels.length; i++) {
if (barrels[i] === grid[x][y].unit) {
isBarrel = true;
break;
}
}
for (var i = 0; i < obstacles.length; i++) {
if (obstacles[i] === grid[x][y].unit && obstacles[i].isDestructible && obstacles[i].obstacleType !== 'box') {
isDestructibleObstacle = true;
break;
}
}
if (isEnemy || isBarrel || isDestructibleObstacle) {
hasValidAttacks = true;
var cell = grid[x][y];
cell.highlighted = true;
if (!cell.attackHighlight) {
cell.attackHighlight = cell.attachAsset('attackRange', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6
});
cell.setChildIndex(cell.attackHighlight, cell.children.length - 1);
}
cell.attackHighlight.visible = true;
}
}
}
}
}
return hasValidAttacks;
}
function clearHighlights() {
for (var x = 0; x < gridWidth; x++) {
for (var y = 0; y < gridHeight; y++) {
var cell = grid[x][y];
cell.unhighlight();
cell.hideSelectedBorder();
if (cell.attackHighlight) {
cell.attackHighlight.visible = false;
}
}
}
}
function handleCellClick(gridX, gridY) {
if (gameState !== 'player_turn') return;
var cell = grid[gridX][gridY];
if (turnPhase === 'select') {
// Select a unit
if (cell.occupied && (cell.unit === knight || cell.unit === archer)) {
if (!cell.unit.hasActed || !cell.unit.hasMoved) {
selectedUnit = cell.unit;
// Show golden border for selected unit
grid[selectedUnit.gridX][selectedUnit.gridY].showSelectedBorder();
turnPhase = 'action';
highlightValidMoves(selectedUnit);
var hasAttacks = highlightValidAttacks(selectedUnit);
instructionText.setText('Click to move and/or attack, then click unit again when done');
}
}
} else if (turnPhase === 'action') {
if (cell.highlighted && !cell.occupied && !selectedUnit.hasMoved) {
// Move to this cell
placeUnit(selectedUnit, gridX, gridY);
selectedUnit.hasMoved = true;
// Play random movement sound
var moveSounds = ['move', 'move1', 'move3'];
var randomSound = moveSounds[Math.floor(Math.random() * moveSounds.length)];
LK.getSound(randomSound).play();
// Check if reached exit portal
if (exitPortal && gridX === exitPortal.gridX && gridY === exitPortal.gridY) {
LK.getSound('victory').play();
LK.playMusic('Victoria');
LK.showYouWin();
return;
}
// After moving, update highlights to show new attack options
clearHighlights();
highlightValidMoves(selectedUnit);
var hasAttacks = highlightValidAttacks(selectedUnit);
// If unit has no valid attacks and has moved, auto-switch to next unit
if (!hasAttacks && selectedUnit.hasMoved) {
// Find next available unit
var nextUnit = null;
if (selectedUnit === knight && archer && (!archer.hasActed || !archer.hasMoved)) {
nextUnit = archer;
} else if (selectedUnit === archer && knight && (!knight.hasActed || !knight.hasMoved)) {
nextUnit = knight;
}
if (nextUnit) {
// Mark current unit as done and switch to next
selectedUnit.hasActed = true;
grid[selectedUnit.gridX][selectedUnit.gridY].hideSelectedBorder();
selectedUnit = nextUnit;
grid[selectedUnit.gridX][selectedUnit.gridY].showSelectedBorder();
highlightValidMoves(selectedUnit);
highlightValidAttacks(selectedUnit);
instructionText.setText('Switched to next unit - Click to move and/or attack');
} else {
instructionText.setText('Click to attack or click unit again when done');
}
} else {
instructionText.setText('Click to attack or click unit again when done');
}
} else if (cell.highlighted && cell.occupied && !selectedUnit.hasActed) {
// Check if it's an enemy, barrel, or destructible obstacle
var isEnemy = false;
var isBarrel = false;
var isDestructibleObstacle = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i] === cell.unit) {
isEnemy = true;
break;
}
}
for (var i = 0; i < barrels.length; i++) {
if (barrels[i] === cell.unit) {
isBarrel = true;
break;
}
}
for (var i = 0; i < obstacles.length; i++) {
if (obstacles[i] === cell.unit && obstacles[i].isDestructible && obstacles[i].obstacleType !== 'box') {
isDestructibleObstacle = true;
break;
}
}
if (isEnemy || isBarrel || isDestructibleObstacle) {
// First click selects target for attack
selectedTarget = cell.unit;
turnPhase = 'confirm_attack';
clearHighlights();
// Highlight only the selected target
cell.highlight();
instructionText.setText('Click target again to confirm attack');
}
} else if (cell.occupied && cell.unit === selectedUnit) {
// Clicking on the selected unit again ends their turn
endPlayerAction();
}
} else if (turnPhase === 'confirm_attack') {
if (cell.occupied && cell.unit === selectedTarget && cell.highlighted) {
// Second click confirms attack
selectedUnit.attack(selectedTarget);
selectedUnit.hasActed = true;
selectedTarget = null;
// After attacking, unit's turn is over
clearHighlights();
endPlayerAction();
} else {
// Click elsewhere cancels attack confirmation
selectedTarget = null;
turnPhase = 'action';
highlightValidMoves(selectedUnit);
highlightValidAttacks(selectedUnit);
instructionText.setText('Click to move and/or attack, then click unit again when done');
}
}
}
function endPlayerAction() {
clearHighlights();
if (selectedUnit) {
// Mark unit as fully done (both moved and acted)
selectedUnit.hasActed = true;
selectedUnit.hasMoved = true;
grid[selectedUnit.gridX][selectedUnit.gridY].hideSelectedBorder();
}
selectedUnit = null;
turnPhase = 'select';
// End Turn button is always available during player turn
endTurnButton.visible = true;
// Check if both units have completed their full turns
var knightDone = !knight || knight.hasActed && knight.hasMoved;
var archerDone = !archer || archer.hasActed && archer.hasMoved;
if (knightDone && archerDone) {
instructionText.setText('All units ready - Click End Turn to continue');
} else {
instructionText.setText('Select next unit to move or Click End Turn to skip');
}
}
function endPlayerTurn() {
endTurnButton.visible = false;
gameState = 'enemy_turn';
turnText.setText('Enemy Turn');
instructionText.setText('Enemies moving...');
// Reset player units
if (knight) {
knight.hasActed = false;
knight.hasMoved = false;
}
if (archer) {
archer.hasActed = false;
archer.hasMoved = false;
}
// Start enemy turn after delay
LK.setTimeout(function () {
executeEnemyTurn();
}, 1000);
}
function executeEnemyTurn() {
var enemyIndex = 0;
function processNextEnemy() {
if (enemyIndex >= enemies.length) {
// All enemies processed, start new player turn
gameState = 'player_turn';
turnText.setText('Player Turn');
instructionText.setText('Select a unit to move');
turnPhase = 'select';
selectedUnit = null;
clearHighlights();
LK.getSound('trumpet').play();
return;
}
var enemy = enemies[enemyIndex];
if (!enemy || enemy.hp <= 0) {
enemyIndex++;
processNextEnemy();
return;
}
// Simple AI: move towards nearest player and attack if in range
var targets = [];
if (knight && knight.hp > 0) targets.push(knight);
if (archer && archer.hp > 0) targets.push(archer);
if (targets.length === 0) {
enemyIndex++;
processNextEnemy();
return;
}
// Find closest target
var closestTarget = targets[0];
var closestDistance = getDistance(enemy.gridX, enemy.gridY, closestTarget.gridX, closestTarget.gridY);
for (var i = 1; i < targets.length; i++) {
var distance = getDistance(enemy.gridX, enemy.gridY, targets[i].gridX, targets[i].gridY);
if (distance < closestDistance) {
closestTarget = targets[i];
closestDistance = distance;
}
}
// Try to attack first
if (closestDistance <= enemy.attackRange) {
if (enemy.attack && typeof enemy.attack === 'function') {
enemy.attack(closestTarget);
}
enemyIndex++;
LK.setTimeout(processNextEnemy, 500);
} else {
// Move towards target
var bestMove = findBestMove(enemy, closestTarget);
if (bestMove) {
// Play random movement sound
var moveSounds = ['move', 'move1', 'move3'];
var randomSound = moveSounds[Math.floor(Math.random() * moveSounds.length)];
LK.getSound(randomSound).play();
moveToPosition(enemy, bestMove.x, bestMove.y, function () {
enemyIndex++;
LK.setTimeout(processNextEnemy, 500);
});
} else {
enemyIndex++;
LK.setTimeout(processNextEnemy, 500);
}
}
}
processNextEnemy();
}
function moveToPosition(unit, targetX, targetY, callback) {
var startX = unit.gridX;
var startY = unit.gridY;
var path = [];
// Simple pathfinding - move one step at a time toward target
var currentX = startX;
var currentY = startY;
while (currentX !== targetX || currentY !== targetY) {
var nextX = currentX;
var nextY = currentY;
if (currentX < targetX) nextX++;else if (currentX > targetX) nextX--;else if (currentY < targetY) nextY++;else if (currentY > targetY) nextY--;
path.push({
x: nextX,
y: nextY
});
currentX = nextX;
currentY = nextY;
}
var stepIndex = 0;
function animateNextStep() {
if (stepIndex >= path.length) {
if (callback) callback();
return;
}
var step = path[stepIndex];
placeUnit(unit, step.x, step.y);
stepIndex++;
LK.setTimeout(animateNextStep, 300);
}
animateNextStep();
}
function findBestMove(enemy, target) {
var bestMove = null;
var bestDistance = getDistance(enemy.gridX, enemy.gridY, target.gridX, target.gridY);
for (var dx = -enemy.moveRange; dx <= enemy.moveRange; dx++) {
for (var dy = -enemy.moveRange; dy <= enemy.moveRange; dy++) {
if (Math.abs(dx) + Math.abs(dy) > enemy.moveRange) continue;
var newX = enemy.gridX + dx;
var newY = enemy.gridY + dy;
if (newX >= 0 && newX < gridWidth && newY >= 0 && newY < gridHeight) {
if (!grid[newX][newY].occupied || newX === enemy.gridX && newY === enemy.gridY) {
var distance = getDistance(newX, newY, target.gridX, target.gridY);
if (distance < bestDistance) {
bestDistance = distance;
bestMove = {
x: newX,
y: newY
};
}
}
}
}
}
return bestMove;
}
function spawnEnemies() {
var enemyTypes = [SkeletonSoldier, SkeletonArcher, SkeletonKnight];
var enemyCount = Math.min(3 + wave, 8);
for (var i = 0; i < enemyCount; i++) {
var enemyType = enemyTypes[Math.floor(Math.random() * enemyTypes.length)];
var enemy = new enemyType();
// Find random empty position on right side of map
var placed = false;
var attempts = 0;
while (!placed && attempts < 50) {
var x = Math.floor(gridWidth * 0.6 + Math.random() * gridWidth * 0.4);
var y = Math.floor(Math.random() * gridHeight);
if (!grid[x][y].occupied) {
placeUnit(enemy, x, y);
enemies.push(enemy);
game.addChild(enemy);
placed = true;
}
attempts++;
}
}
}
function spawnBarrels() {
var barrelCount = Math.min(2 + Math.floor(wave / 2), 4);
for (var i = 0; i < barrelCount; i++) {
var barrel = new Barrel();
// Find random empty position in middle area of map
var placed = false;
var attempts = 0;
while (!placed && attempts < 50) {
var x = Math.floor(gridWidth * 0.3 + Math.random() * gridWidth * 0.4);
var y = Math.floor(Math.random() * gridHeight);
if (!grid[x][y].occupied) {
placeUnit(barrel, x, y);
barrels.push(barrel);
game.addChild(barrel);
placed = true;
}
attempts++;
}
}
}
function spawnObstacles() {
var obstacleTypes = ['stone', 'box', 'column'];
var obstacleCount = Math.min(3 + Math.floor(wave / 3), 6);
for (var i = 0; i < obstacleCount; i++) {
var obstacle = new Obstacle();
var type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
obstacle.init(type);
// Find random empty position across the map
var placed = false;
var attempts = 0;
while (!placed && attempts < 50) {
var x = Math.floor(Math.random() * gridWidth);
var y = Math.floor(Math.random() * gridHeight);
// Avoid placing near starting positions and exit
if (x > 2 && x < gridWidth - 3 && !grid[x][y].occupied) {
placeUnit(obstacle, x, y);
obstacles.push(obstacle);
game.addChild(obstacle);
placed = true;
}
attempts++;
}
}
}
function checkVictory() {
if (enemies.length === 0) {
LK.getSound('trompeta').play();
LK.playMusic('Victoria');
LK.showYouWin();
return;
}
}
function showHealthInterface(unit) {
// Remove existing health interface if any
if (game.healthInterface) {
game.healthInterface.destroy();
game.healthInterface = null;
}
// Create health interface
var healthInterface = new Container();
var background = LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 0.8,
tint: 0x000000
});
background.alpha = 0.8;
healthInterface.addChild(background);
// Health text
var healthText = new Text2('HP: ' + unit.hp + '/' + unit.maxHp, {
size: 40,
fill: 0xFFFFFF
});
healthText.anchor.set(0.5, 0.5);
healthInterface.addChild(healthText);
// Position interface next to end turn button, optimized for full screen
healthInterface.x = endTurnButton.x - 350;
healthInterface.y = endTurnButton.y;
game.addChild(healthInterface);
game.healthInterface = healthInterface;
// Auto-hide after 3 seconds
LK.setTimeout(function () {
if (game.healthInterface === healthInterface) {
healthInterface.destroy();
game.healthInterface = null;
}
}, 3000);
}
function checkGameOver() {
if ((!knight || knight.hp <= 0) && (!archer || archer.hp <= 0)) {
LK.showGameOver();
}
}
function showComicStory() {
if (currentComicPanel < comicStory.length) {
// Start comic music on first panel
if (currentComicPanel === 0) {
LK.playMusic('comicMusic');
}
var panel = new ComicPanel();
var storyData = comicStory[currentComicPanel];
panel.init(currentComicPanel + 1, storyData.title, storyData.story);
panel.x = 2048 / 2;
panel.y = 2732 / 2;
game.addChild(panel);
comicContainer = panel;
currentComicPanel++;
} else {
// Comic finished, start the game
startActualGame();
}
}
game.showNextComicPanel = function () {
if (comicContainer) {
comicContainer = null;
}
showComicStory();
};
function startActualGame() {
showingComic = false;
// Stop comic music and start battle music
LK.stopMusic();
LK.playMusic('battleMusic');
// Initialize the actual game
initializeGameplay();
}
function initializeGameplay() {
// Initialize grid
initializeGrid();
// Create and place heroes
knight = new Knight();
archer = new Archer();
game.addChild(knight);
game.addChild(archer);
placeUnit(knight, 1, 3);
placeUnit(archer, 1, 4);
// Create exit portal
exitPortal = LK.getAsset('exitPortal', {
anchorX: 0.5,
anchorY: 0.5
});
exitPortal.gridX = gridWidth - 2;
exitPortal.gridY = 3;
exitPortal.x = grid[exitPortal.gridX][exitPortal.gridY].x;
exitPortal.y = grid[exitPortal.gridX][exitPortal.gridY].y;
exitPortal.alpha = 0.7;
game.addChild(exitPortal);
// Spawn initial enemies
spawnEnemies();
// Spawn initial barrels
spawnBarrels();
// Spawn initial obstacles
spawnObstacles();
// Make End Turn button available from game start
endTurnButton.visible = true;
// Play trumpet sound to signal game start
LK.getSound('trumpet').play();
}
// Start with comic story
showComicStory();
game.update = function () {
// Show/hide UI based on comic state
if (showingComic) {
turnText.visible = false;
waveText.visible = false;
instructionText.visible = false;
endTurnButton.visible = false;
} else {
turnText.visible = true;
waveText.visible = true;
instructionText.visible = true;
// endTurnButton visibility handled by game logic
}
};
Un portal de color morado que tiene pinta de vortice. In-Game asset. 2d. High contrast. No shadows
caballero zorro, con escudo y lanza. In-Game asset. 2d. High contrast. No shadows
conejo con archo y flecha mirando hacia la derecha y capucha tiene. In-Game asset. 2d. High contrast. No shadows
arquero esqueleto. In-Game asset. High contrast. No shadows
un esqueleto con espada y escudo. In-Game asset. 2d. High contrast. No shadows
un esqueleto grande, con un hacha, un caso y escudo. In-Game asset. 2d. High contrast. No shadows
un botton que dice "Fin del Turno" con letras blancas y grandes. In-Game asset. 2d. High contrast. No shadows
cesped. In-Game asset. 2d. High contrast. No shadows
un barril rojo de era medieval. In-Game asset. 2d. High contrast. No shadows
una caja de madera de color marron. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
una columna de piedra. In-Game asset. 2d. High contrast. No shadows
rocas grande. In-Game asset. 2d. High contrast. No shadows
esqueletos guerreros que salen de la tierra por un brujo, miesntras un ballero zorro y un conejo arquero miran asombrado. In-Game asset. 2d. High contrast. No shadows
un hechizero esqueleto les mira de forma aterradora hacia adelante con ojos rojos y brillosos. In-Game asset. 2d. High contrast. No shadows
un caballero zorro y un arquero conejo caminan por el bosque tranquilamente, mientras en el fondo en la oscuridad del bosque se ven unos ojos rojos que resaltan. In-Game asset. 2d. High contrast. No shadows