User prompt
Quita cuadros de río para hacerlos cuadros de puente (de 2 de horizontal y 3 de vertical) 2 puentes a lados opuestos del mapa.
User prompt
¿Y los 2 puentes? solo veo el río, así las unidades terrestres no van a pasar; y los indicadores de unidades deben estar abajo, ya que el botón de pausa no deja verlos
User prompt
De nuevo, te explicaré el puente ( en horizontal cuadro de río, cuadro de puente, cuadro de puente, de ahí 9 de río, cuadro de puente, cuadro de puente, y cuadro de río, y de ahí en vertical los copias hasta que sea de 3 en vertical), y que no haya árboles en frente de un puente, y mis unidades son en la esquina inferior derecha en vista de espejo a las unidades enemigas que estarán en la esquina superior derecha
User prompt
Ensancha algo más el río (3 de vertical) y pon los puentes en el río como dije (de 2 de horizontal por 3 de vertical), y quiero mis unidades aliadas al extremo derecho del mapa. Por cierto, el filtro de color que diferencia mis unidades de las enemigas (rojo enemigos y azul aliados) hazlo más opaco en el sprite y sin fondo, además de que una unidad aliada movida se vuelva gris, para así saber cuales ya se movieron, así como un contador de Unidades aliadas y otro de enemigas (8/8 rojo para enemigos y azul para aliados, reduciéndose en una cada que se pierda una unidad: 7/8, 6/8...) ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Bien, lo siguiente es que quiero que ambos bandos estén en lados opuestos del mapa, que están muy cerca. De ahí sigue el terreno: Normal (no afecta la movilidad) Árbol (los Jinetes no lo cruzan, los Jinetes de Dragón los pasan de largo como si fuese terreno normal, y si una unidad lo cruza, no se mueve más hasta el siguiente turno), Agua (quiero un río en medio del mapa, el cual solo el jinete de dragón puede cruzar, pero los demás no), y 2 puentes (uno a cada extremo del río que funciona igual como el terreno normal, pero no tan pegados al borde del mapa, sino algo alejados). Por último, al mover todas tus unidades, el turno termina automáticamente. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ahora, necesito un mapa más grande, que la opción de terminar turno no aparezca cuando salga la opción de atacar/curar o ignorar ni cuando sea el turno del oponente (para que no estorbe) y quiero que cuando salga la opción de atacar/curar o ignorar, al hacer clic en algo que no sea las opciones, la unidad vuelva a su posición original (para así moverla de nuevo a otra zona), ah, y que al clicar en una unidad enemiga, se vea su cuadrícula de movimiento y se quite al clicarla de nuevo. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
El Healer no cura, quiero que funcione igual que lo de atacar o ignorar, pero que sea Curar o Ignorar, además, quiero que se pueda cancelar la opción de mover una unidad seleccionada haciendo clic de nuevo en cualquier parte. Ah, y quiero que a cada unidad le demos su propio sprite por clase, no por bando. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Veamos, ahora quiero añadir más unidades: -Jinete (Avanza 3 casillas) -Jinete de dragón (Avanza 3 casillas) -Healer (No ataca, sino que cura 1 de HP al estar al lado de un aliado) -Acorazado (Solo avanza una casilla) Y dale a todos 2 de HP y 1 de daño para probar al Healer
User prompt
Please fix the bug: 'TypeError: Cannot use 'in' operator to search for 'x' in null' in or related to this line: 'tween(selectedUnit, {' Line Number: 435 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Bien, para diferenciar los equipos, pon un contorno rojo en los enemigos y azul en los jugables, además de una mini corona en la esquina inferior derecha del comandante de ambos, para saber cual es el comandante. Y añade un efecto al atacar (que el que ataca vibre y que el derrotado se desvanezca) ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Todo bien, pero falta que, al derrotar al commander enemigo debería hacer ganar, y si te derrotan al tuyo pierdes
User prompt
Todo bien, pero el enemigo no se mueve solo en su turno, sino que lo controlo yo, corrígelo dándole una IA al enemigo, y también, el bow no puede atacar a una casilla de distancia, solo a 2, o sea, no ataca cuerpo a cuerpo como el espada y el mago ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'to')' in or related to this line: 'tween(unit).to({' Line Number: 321 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'to')' in or related to this line: 'tween(unit).to({' Line Number: 321
User prompt
No se ve nada
User prompt
Please fix the bug: 'Timeout.tick error: Cannot read properties of null (reading 'canAttack')' in or related to this line: 'if (selectedUnit.canAttack(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team !== selectedUnit.team && grid[ty][tx].unit.hp > 0) {' Line Number: 244
Code edit (1 edits merged)
Please save this source code
User prompt
Sigue sin funcionar, te daré una explicación simple del movimiento de una unidad 1.- Seleccionar unidad: -El jugador hace clic en una unidad propia. -En ese momento, se iluminan en la cuadrícula todas las casillas a las que puede moverse (según su rango de movimiento). 2.- Elegir destino: -El jugador hace clic en una casilla vacía dentro de ese rango. -La unidad se mueve automáticamente hasta esa casilla. 3.- Acción posterior al movimiento: -Si después del movimiento hay un enemigo dentro de su rango de ataque, aparece un pequeño menú con dos opciones: Atacar: el jugador hace clic y el combate ocurre. Ignorar: la unidad termina su turno sin atacar. -Si no hay enemigos cerca, el turno de la unidad simplemente termina al llegar a la casilla. Bien, corrige el código. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Sigo sin moverme como te dije, le hago clic a mi unidad y no me sale nada, que al hacer clic a mi unidad, aparezca una cuadrícula rojiza transparente para saber a donde se mueve cada uno al clicarlo
User prompt
No me puedo mover, lo que quiero es que al hacer clic en una unidad mía, me salga a donde lo puedo mover, siendo un máximo de 2 casillas, y de ahí se mueve a la casilla designada con otro clic, si hay una unidad cerca de su rango de ataque (1 casilla para las espadas, 2 para los arcos y 1 o 2 para los magos) me salga el botón atacar o ignorar, y quiero la cuadrícula más grande, de momento que las unidades mueran al primer daño.
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'toGlobal')' in or related to this line: 'var guiPos = LK.gui.toLocal(obj.parent.toGlobal(obj.position));' Line Number: 397
Code edit (1 edits merged)
Please save this source code
User prompt
Tactical Commander
Initial prompt
Bien, quiero un juego de estrategia por turnos en cuadrícula, donde controlas a un pequeño grupo de héroes con distintas habilidades y armas. Cada turno, eliges cómo mover a tus personajes por el mapa y a quién atacar, aprovechando el terreno para obtener ventajas defensivas o ofensivas. El enemigo también mueve sus tropas con la misma lógica, por lo que debes planear con cuidado, proteger a tus aliados más débiles y usar la posición a tu favor. El combate es sencillo: comparas el poder de ataque de una unidad contra la defensa del oponente, con distintos rangos de alcance según el arma (espadas de corto alcance, arcos a distancia, magia que atacan cerca y lejos, etc.). La partida se desarrolla en un único mapa con un objetivo claro, derrotar al líder enemigo. Si tu comandante cae, pierdes la batalla. Es un juego de estrategia táctica ligera, con personajes genéricos, reglas simples y un escenario único, diseñado para transmitir la tensión de pensar cada movimiento como si fuera decisivo.
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var GridTile = Container.expand(function (gridX, gridY) {
var self = Container.call(this);
self.gridX = gridX;
self.gridY = gridY;
self.unit = null;
self.hasTerrain = false;
var tileGraphics = self.attachAsset('gridTile', {
anchorX: 0.5,
anchorY: 0.5
});
tileGraphics.alpha = 0; // Make gridTile transparent so terrain is visible
self.terrainType = 'normal'; // normal, tree, water, bridge
self.terrainGraphics = null; // Store terrain graphics reference
self.addTerrain = function (terrainType) {
// Remove existing terrain graphics if present
if (self.terrainGraphics) {
self.removeChild(self.terrainGraphics);
self.terrainGraphics = null;
}
self.terrainType = terrainType || 'normal';
var assetName = 'terrain';
if (terrainType === 'tree') {
assetName = 'tree';
} else if (terrainType === 'water') {
assetName = 'water';
} else if (terrainType === 'bridge') {
assetName = 'bridge';
} else if (terrainType === 'normal') {
assetName = 'terrain';
}
self.terrainGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// For trees, make them fully opaque and visible, for other terrain make slightly transparent
if (terrainType === 'tree') {
self.terrainGraphics.alpha = 1.0;
} else {
self.terrainGraphics.alpha = 0.8;
}
// Move terrain graphics to bottom of display list so highlights show on top
self.setChildIndex(self.terrainGraphics, 0);
self.hasTerrain = true;
};
// Add highlight graphics AFTER terrain so they appear on top
var highlightGraphics = self.attachAsset('highlightTile', {
anchorX: 0.5,
anchorY: 0.5
});
highlightGraphics.alpha = 0;
var attackGraphics = self.attachAsset('attackTile', {
anchorX: 0.5,
anchorY: 0.5
});
attackGraphics.alpha = 0;
var attackRangeGraphics = self.attachAsset('attackRangeTile', {
anchorX: 0.5,
anchorY: 0.5
});
attackRangeGraphics.alpha = 0;
var healRangeGraphics = self.attachAsset('healRangeTile', {
anchorX: 0.5,
anchorY: 0.5
});
healRangeGraphics.alpha = 0;
self.showHighlight = function () {
highlightGraphics.alpha = 0.4;
attackGraphics.alpha = 0;
};
self.showAttackRange = function () {
attackGraphics.alpha = 0.5;
highlightGraphics.alpha = 0;
};
self.showAttackRangePreview = function () {
attackRangeGraphics.alpha = 0.3;
};
self.showHealRangePreview = function () {
healRangeGraphics.alpha = 0.3;
};
self.hideHighlights = function () {
highlightGraphics.alpha = 0;
attackGraphics.alpha = 0;
attackRangeGraphics.alpha = 0;
healRangeGraphics.alpha = 0;
};
return self;
});
var Unit = Container.expand(function (type, team, weaponType, swordVariant, unitName) {
var self = Container.call(this);
self.type = type;
self.team = team;
self.weaponType = weaponType;
self.swordVariant = swordVariant || 1; // Default to sword1 if not specified
self.name = unitName || 'Unknown';
self.gridX = 0;
self.gridY = 0;
// Set stats based on unit name and type
if (unitName === 'Ardan') {
self.hp = 40;
self.attack = 19;
} else if (unitName === 'Kael') {
self.hp = 28;
self.attack = 13;
} else if (unitName === 'Hikari') {
self.hp = 32;
self.attack = 14;
} else if (unitName === 'Brutus') {
self.hp = 52;
self.attack = 15;
} else if (unitName === 'Mizuki') {
self.hp = 34;
self.attack = 12;
} else if (unitName === 'Lyra') {
self.hp = 35;
self.attack = 16;
} else if (unitName === 'Darius') {
self.hp = 34;
self.attack = 16;
} else if (unitName === 'Eirynn') {
self.hp = 26;
self.attack = 10; // Healer uses attack stat for healing power
} else if (unitName === 'Elian') {
self.hp = 27;
self.attack = 13;
} else if (unitName === 'Irios') {
self.hp = 30;
self.attack = 17;
} else {
// Default stats for unnamed units
self.hp = 2;
self.attack = 1;
}
self.maxHp = self.hp;
self.defense = 1;
self.hasMoved = false;
self.hasAttacked = false;
var unitGraphics;
if (type === 'commander') {
unitGraphics = self.attachAsset('commander', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'rider') {
unitGraphics = self.attachAsset('rider', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'dragonRider') {
unitGraphics = self.attachAsset('dragonRider', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'healer') {
unitGraphics = self.attachAsset('healer', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'armored') {
unitGraphics = self.attachAsset('armored', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'manakete') {
unitGraphics = self.attachAsset('manakete', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
// Use weapon type as sprite
if (weaponType === 'sword') {
var swordAsset = 'sword' + self.swordVariant; // Use sword1 or sword2
unitGraphics = self.attachAsset(swordAsset, {
anchorX: 0.5,
anchorY: 0.5
});
} else if (weaponType === 'bow') {
unitGraphics = self.attachAsset('bow', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (weaponType === 'magic') {
unitGraphics = self.attachAsset('magic', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
var swordAsset = 'sword' + (self.swordVariant || 1); // Default to sword1
unitGraphics = self.attachAsset(swordAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
}
// Add team color filter directly to unit graphics (more opaque, no background)
if (team === 'player') {
unitGraphics.tint = 0x5555FF; // Blue tint for player units
} else {
unitGraphics.tint = 0xFF5555; // Red tint for enemy units
}
// Add commander crown
if (type === 'commander') {
var crown = new Text2('♛', {
size: 25,
fill: 0xFFD700
});
crown.anchor.set(1, 1);
crown.x = 50;
crown.y = 50;
self.addChild(crown);
}
self.updateHpDisplay = function () {
if (self.hp <= 0) {
unitGraphics.alpha = 0.3;
}
};
self.takeDamage = function (damage, attackerWeaponType) {
var baseDamage = damage; // Use the actual attack stat passed as parameter
// Apply specific weapon advantage bonuses
if (attackerWeaponType === 'bow' && self.type === 'dragonRider') {
baseDamage = damage + 1; // Bow does +1 bonus damage to Dragon Rider
} else if (attackerWeaponType === 'magic' && self.type === 'armored') {
baseDamage = damage + 1; // Magic does +1 bonus damage to Armored
}
// Calculate final damage after defense
var actualDamage = Math.max(1, baseDamage - self.defense);
if (grid[self.gridY][self.gridX].hasTerrain) {
actualDamage = Math.max(1, actualDamage - 1);
}
self.hp -= actualDamage;
self.updateHpDisplay();
return self.hp <= 0;
};
self.getRange = function () {
if (self.weaponType === 'sword') {
return 1;
}
if (self.weaponType === 'bow') {
return 2;
}
if (self.weaponType === 'magic') {
return 2;
}
if (self.weaponType === 'rider') {
return 1;
}
if (self.weaponType === 'dragonRider') {
return 1;
}
if (self.weaponType === 'healer') {
return 1;
}
if (self.weaponType === 'armored') {
return 2;
}
return 1;
};
self.getMovementRange = function () {
if (self.weaponType === 'rider' || self.weaponType === 'dragonRider') {
return 5;
}
if (self.weaponType === 'manakete') {
return 4;
}
if (self.weaponType === 'armored') {
return 3;
}
return 4; // Default movement for sword, bow, magic, healer
};
self.canAttack = function (targetX, targetY) {
var distance = Math.abs(self.gridX - targetX) + Math.abs(self.gridY - targetY);
if (self.weaponType === 'healer') {
return false;
} // Healer cannot attack
if (self.weaponType === 'bow') {
return distance === 2; // Bow can only attack at exactly range 2, not adjacent
}
if (self.weaponType === 'manakete') {
return distance >= 1 && distance <= 2; // Manakete can attack at range 1-2 like magic
}
return distance <= self.getRange();
};
self.canHeal = function (targetX, targetY) {
if (self.weaponType !== 'healer') {
return false;
}
var distance = Math.abs(self.gridX - targetX) + Math.abs(self.gridY - targetY);
return distance === 1; // Healer can heal adjacent allies
};
self.heal = function (target) {
if (target.hp < target.maxHp) {
target.hp += 10;
if (target.hp > target.maxHp) {
target.hp = target.maxHp;
}
target.updateHpDisplay();
LK.getSound('HealSound').play();
showHealEffect(target);
return true;
}
return false;
};
self.resetTurn = function () {
self.hasMoved = false;
self.hasAttacked = false;
unitGraphics.alpha = 1.0;
// Restore original team color
if (self.team === 'player') {
unitGraphics.tint = 0x5555FF; // Blue tint for player units
} else {
unitGraphics.tint = 0xFF5555; // Red tint for enemy units
}
};
self.counterAttack = function (attacker) {
if (self.hp <= 0) return false; // Can't counterattack if dead
if (!self.canAttack(attacker.gridX, attacker.gridY)) return false; // Must be in range
// Add counterattack shake animation
var originalX = self.x;
tween(self, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(self, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(self, {
x: originalX
}, {
duration: 100
});
}
});
}
});
showAttackEffect(attacker);
var isDead = attacker.takeDamage(self.attack, self.weaponType);
return isDead;
};
self.endTurn = function () {
self.hasMoved = true;
self.hasAttacked = true;
unitGraphics.alpha = 0.6;
// Add gray tint for moved allied units
if (self.team === 'player') {
unitGraphics.tint = 0x888888; // Gray tint for moved player units
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2d2d2d
});
/****
* Game Code
****/
// Add background field
var backgroundField = LK.getAsset('field', {
anchorX: 0,
anchorY: 0,
alpha: 0.7
});
backgroundField.x = 0;
backgroundField.y = 0;
game.addChild(backgroundField);
var GRID_SIZE = 15;
var TILE_SIZE = 128;
var grid = [];
var playerUnits = [];
var enemyUnits = [];
var selectedUnit = null;
var originalUnitPosition = null; // Store original position when moving
var selectedEnemyUnit = null; // Track selected enemy for movement display
var currentTurn = 'player';
var gameState = 'menu'; // Start in menu state instead of playing
var turnPhase = 'select'; // 'select', 'move', 'chooseAction', 'attack'
// UI
var turnText = new Text2('Player Turn', {
size: 60,
fill: 0xFFFFFF
});
turnText.anchor.set(0.5, 0);
turnText.alpha = 0; // Hidden initially
LK.gui.top.addChild(turnText);
var phaseText = new Text2('Select Unit', {
size: 40,
fill: 0xFFFFFF
});
phaseText.anchor.set(0.5, 0);
phaseText.y = 80;
phaseText.alpha = 0; // Hidden initially
LK.gui.top.addChild(phaseText);
// Unit counters
var playerCounter = new Text2('10/10', {
size: 35,
fill: 0x5555FF
});
playerCounter.anchor.set(0, 1);
playerCounter.x = 20;
playerCounter.y = -120;
playerCounter.alpha = 0; // Hidden initially
LK.gui.bottomLeft.addChild(playerCounter);
var enemyCounter = new Text2('10/10', {
size: 35,
fill: 0xFF5555
});
enemyCounter.anchor.set(1, 1);
enemyCounter.x = -20;
enemyCounter.y = -120;
enemyCounter.alpha = 0; // Hidden initially
LK.gui.bottomRight.addChild(enemyCounter);
var endTurnBtn = new Text2('End Turn', {
size: 50,
fill: 0x00FF00
});
endTurnBtn.anchor.set(0.5, 1);
endTurnBtn.alpha = 0; // Hidden initially
endTurnBtn.interactive = false; // Disabled initially
LK.gui.bottom.addChild(endTurnBtn);
// Unit info panel
var unitInfoPanel = new Container();
unitInfoPanel.x = -20;
unitInfoPanel.y = 120;
LK.gui.topRight.addChild(unitInfoPanel);
var unitInfoBg = LK.getAsset('terrain', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.5,
scaleY: 2.2
});
unitInfoBg.tint = 0x444444;
unitInfoBg.alpha = 0.8;
unitInfoPanel.addChild(unitInfoBg);
var unitInfoImage = null;
var unitInfoName = new Text2('', {
size: 32,
fill: 0xFFD700
});
unitInfoName.anchor.set(1, 0);
unitInfoName.x = -20;
unitInfoName.y = -90;
unitInfoPanel.addChild(unitInfoName);
var unitInfoClass = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
unitInfoClass.anchor.set(1, 0);
unitInfoClass.x = -20;
unitInfoClass.y = -60;
unitInfoPanel.addChild(unitInfoClass);
var unitInfoHP = new Text2('', {
size: 25,
fill: 0x00FF00
});
unitInfoHP.anchor.set(1, 0);
unitInfoHP.x = -20;
unitInfoHP.y = -25;
unitInfoPanel.addChild(unitInfoHP);
var unitInfoDamage = new Text2('', {
size: 25,
fill: 0xFF4444
});
unitInfoDamage.anchor.set(1, 0);
unitInfoDamage.x = -20;
unitInfoDamage.y = 10;
unitInfoPanel.addChild(unitInfoDamage);
unitInfoPanel.alpha = 0;
unitInfoPanel.visible = false; // Hide completely initially
function showUnitInfo(unit) {
if (unitInfoImage) {
unitInfoPanel.removeChild(unitInfoImage);
unitInfoImage = null;
}
var assetName = 'sword';
if (unit.type === 'commander') {
assetName = 'commander';
} else if (unit.type === 'rider') {
assetName = 'rider';
} else if (unit.type === 'dragonRider') {
assetName = 'dragonRider';
} else if (unit.type === 'healer') {
assetName = 'healer';
} else if (unit.type === 'armored') {
assetName = 'armored';
} else if (unit.type === 'manakete') {
assetName = 'manakete';
} else if (unit.weaponType === 'sword') {
assetName = 'sword' + unit.swordVariant; // Use sword1 or sword2
} else if (unit.weaponType === 'bow') {
assetName = 'bow';
} else if (unit.weaponType === 'magic') {
assetName = 'magic';
}
unitInfoImage = LK.getAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
var className = unit.type === 'commander' ? 'Commander' : unit.type === 'rider' ? 'Rider' : unit.type === 'dragonRider' ? 'Dragon Rider' : unit.type === 'healer' ? 'Healer' : unit.type === 'armored' ? 'Armored' : unit.type === 'manakete' ? 'Manakete' : unit.weaponType === 'sword' ? 'Sword' : unit.weaponType === 'bow' ? 'Bow' : unit.weaponType === 'magic' ? 'Magic' : 'Hero';
// Position image further left based on class name length to prevent overlap
var imageOffset = -220 - className.length * 8;
unitInfoImage.x = imageOffset;
unitInfoImage.y = -20;
// Remove tint from unit info image
unitInfoImage.tint = 0xFFFFFF;
unitInfoPanel.addChild(unitInfoImage);
unitInfoName.setText(unit.name);
unitInfoClass.setText(className);
unitInfoHP.setText('HP: ' + unit.hp + '/' + unit.maxHp);
if (unit.weaponType === 'healer') {
unitInfoDamage.setText('HEAL: ' + unit.attack);
} else {
unitInfoDamage.setText('ATK: ' + unit.attack);
}
unitInfoPanel.alpha = 1;
}
function hideUnitInfo() {
unitInfoPanel.alpha = 0;
}
// botones dinámicos de acción
var attackBtn = null;
var ignoreBtn = null;
// Action button variables
var danceBtn = null;
function showActionButtons() {
// Hide end turn button when showing action buttons
endTurnBtn.alpha = 0;
endTurnBtn.interactive = false;
if (selectedUnit.weaponType === 'healer') {
// Check if there are valid heal targets
var hasHealTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canHeal(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team === selectedUnit.team && grid[ty][tx].unit.hp < grid[ty][tx].unit.maxHp) {
hasHealTargets = true;
break;
}
}
if (hasHealTargets) {
break;
}
}
if (hasHealTargets) {
attackBtn = new Text2('Heal', {
size: 50,
fill: 0x00FF00
});
attackBtn.anchor.set(0.5, 1);
attackBtn.y = -60;
LK.gui.bottom.addChild(attackBtn);
attackBtn.interactive = true;
attackBtn.down = function () {
turnPhase = 'heal';
phaseText.setText('Choose ally to heal');
showHealOptions(selectedUnit);
removeActionButtons();
};
}
} else {
attackBtn = new Text2('Attack', {
size: 50,
fill: 0xFF0000
});
attackBtn.anchor.set(0.5, 1);
attackBtn.y = -60;
LK.gui.bottom.addChild(attackBtn);
attackBtn.interactive = true;
attackBtn.down = function () {
turnPhase = 'attack';
phaseText.setText('Choose enemy to attack');
showAttackOptions(selectedUnit);
removeActionButtons();
};
}
ignoreBtn = new Text2('Ignore', {
size: 50,
fill: 0xFFFF00
});
ignoreBtn.anchor.set(0.5, 1);
ignoreBtn.y = -10;
LK.gui.bottom.addChild(ignoreBtn);
ignoreBtn.interactive = true;
ignoreBtn.down = function () {
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
removeActionButtons();
hideUnitInfo();
// Check if all player units have moved
checkAutoEndTurn();
};
}
function removeActionButtons() {
// Show end turn button again
if (currentTurn === 'player') {
endTurnBtn.alpha = 1;
endTurnBtn.interactive = true;
}
if (attackBtn) {
LK.gui.bottom.removeChild(attackBtn);
attackBtn = null;
}
if (ignoreBtn) {
LK.gui.bottom.removeChild(ignoreBtn);
ignoreBtn = null;
}
}
// Initialize grid
function initializeGrid() {
var startX = (2048 - GRID_SIZE * TILE_SIZE) / 2;
var startY = (2732 - GRID_SIZE * TILE_SIZE) / 2;
for (var y = 0; y < GRID_SIZE; y++) {
grid[y] = [];
for (var x = 0; x < GRID_SIZE; x++) {
var tile = new GridTile(x, y);
tile.x = startX + x * TILE_SIZE + TILE_SIZE / 2;
tile.y = startY + y * TILE_SIZE + TILE_SIZE / 2;
grid[y][x] = tile;
game.addChild(tile);
}
}
// Generate terrain
generateTerrain();
// Add border frame around the map
var borderThickness = 20;
var mapStartX = (2048 - GRID_SIZE * TILE_SIZE) / 2;
var mapStartY = (2732 - GRID_SIZE * TILE_SIZE) / 2;
var mapWidth = GRID_SIZE * TILE_SIZE;
var mapHeight = GRID_SIZE * TILE_SIZE;
// Create border graphics using terrain assets
// Top border
var topBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: (mapWidth + borderThickness * 2) / 132,
scaleY: borderThickness / 132
});
topBorder.x = mapStartX - borderThickness;
topBorder.y = mapStartY - borderThickness;
topBorder.tint = 0x8B4513;
game.addChild(topBorder);
// Bottom border
var bottomBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: (mapWidth + borderThickness * 2) / 132,
scaleY: borderThickness / 132
});
bottomBorder.x = mapStartX - borderThickness;
bottomBorder.y = mapStartY + mapHeight;
bottomBorder.tint = 0x8B4513;
game.addChild(bottomBorder);
// Left border
var leftBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: borderThickness / 132,
scaleY: mapHeight / 132
});
leftBorder.x = mapStartX - borderThickness;
leftBorder.y = mapStartY;
leftBorder.tint = 0x8B4513;
game.addChild(leftBorder);
// Right border
var rightBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: borderThickness / 132,
scaleY: mapHeight / 132
});
rightBorder.x = mapStartX + mapWidth;
rightBorder.y = mapStartY;
rightBorder.tint = 0x8B4513;
game.addChild(rightBorder);
}
// Initialize units
function initializeUnits() {
// Player units (bottom-right corner)
var playerCommander = new Unit('commander', 'player', 'sword', 1, 'Ardan');
placeUnit(playerCommander, 12, 14);
playerUnits.push(playerCommander);
var playerSword1 = new Unit('hero', 'player', 'sword', 1, 'Kael');
placeUnit(playerSword1, 13, 13);
playerUnits.push(playerSword1);
var playerSword2 = new Unit('hero', 'player', 'sword', 2, 'Hikari');
placeUnit(playerSword2, 9, 14);
playerUnits.push(playerSword2);
var playerArmored = new Unit('armored', 'player', 'armored', 1, 'Brutus');
placeUnit(playerArmored, 14, 13);
playerUnits.push(playerArmored);
var playerMagic = new Unit('hero', 'player', 'magic', 1, 'Mizuki');
placeUnit(playerMagic, 9, 13);
playerUnits.push(playerMagic);
var playerDragonRider = new Unit('dragonRider', 'player', 'dragonRider', 1, 'Lyra');
placeUnit(playerDragonRider, 8, 12);
playerUnits.push(playerDragonRider);
var playerRider = new Unit('rider', 'player', 'rider', 1, 'Darius');
placeUnit(playerRider, 14, 12);
playerUnits.push(playerRider);
var playerHealer = new Unit('healer', 'player', 'healer', 1, 'Eirynn');
placeUnit(playerHealer, 10, 14);
playerUnits.push(playerHealer);
var playerBow = new Unit('hero', 'player', 'bow', 1, 'Elian');
placeUnit(playerBow, 11, 13);
playerUnits.push(playerBow);
var playerManakete = new Unit('manakete', 'player', 'manakete', 1, 'Irios');
placeUnit(playerManakete, 11, 12);
playerUnits.push(playerManakete);
// Enemy units (top-left corner)
var enemyCommander = new Unit('commander', 'enemy', 'sword', 1, 'Ardan');
placeUnit(enemyCommander, 2, 0);
enemyUnits.push(enemyCommander);
var enemySword1 = new Unit('hero', 'enemy', 'sword', 1, 'Kael');
placeUnit(enemySword1, 1, 1);
enemyUnits.push(enemySword1);
var enemySword2 = new Unit('hero', 'enemy', 'sword', 2, 'Hikari');
placeUnit(enemySword2, 5, 0);
enemyUnits.push(enemySword2);
var enemyArmored = new Unit('armored', 'enemy', 'armored', 1, 'Brutus');
placeUnit(enemyArmored, 0, 1);
enemyUnits.push(enemyArmored);
var enemyMagic = new Unit('hero', 'enemy', 'magic', 1, 'Mizuki');
placeUnit(enemyMagic, 5, 1);
enemyUnits.push(enemyMagic);
var enemyDragonRider = new Unit('dragonRider', 'enemy', 'dragonRider', 1, 'Lyra');
placeUnit(enemyDragonRider, 6, 2);
enemyUnits.push(enemyDragonRider);
var enemyRider = new Unit('rider', 'enemy', 'rider', 1, 'Darius');
placeUnit(enemyRider, 0, 2);
enemyUnits.push(enemyRider);
var enemyHealer = new Unit('healer', 'enemy', 'healer', 1, 'Eirynn');
placeUnit(enemyHealer, 4, 0);
enemyUnits.push(enemyHealer);
var enemyBow = new Unit('hero', 'enemy', 'bow', 1, 'Elian');
placeUnit(enemyBow, 3, 1);
enemyUnits.push(enemyBow);
var enemyManakete = new Unit('manakete', 'enemy', 'manakete', 1, 'Irios');
placeUnit(enemyManakete, 3, 2);
enemyUnits.push(enemyManakete);
}
function generateTerrain() {
// First, add normal terrain to all tiles
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x].addTerrain('normal');
}
}
// Add water river in middle of map (rows 6, 7, 8 - 3 tiles wide)
for (var y = 6; y <= 8; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x].addTerrain('water');
}
}
// Replace water tiles with bridge tiles at specified positions
// Bridge 1: columns 1-2, rows 6-8 (2 horizontal x 3 vertical)
for (var y = 6; y <= 8; y++) {
for (var x = 1; x <= 2; x++) {
grid[y][x].addTerrain('bridge');
}
}
// Bridge 2: columns 12-13, rows 6-8 (2 horizontal x 3 vertical)
for (var y = 6; y <= 8; y++) {
for (var x = 12; x <= 13; x++) {
grid[y][x].addTerrain('bridge');
}
}
// Add some trees scattered around (avoiding bridge areas) - trees go on top of normal terrain
var treePositions = [{
x: 4,
y: 4
}, {
x: 6,
y: 5
}, {
x: 9,
y: 4
}, {
x: 11,
y: 5
}, {
x: 3,
y: 10
}, {
x: 5,
y: 11
}, {
x: 8,
y: 10
}, {
x: 10,
y: 11
}, {
x: 7,
y: 3
}, {
x: 14,
y: 10
}, {
x: 0,
y: 12
}, {
x: 14,
y: 4
}];
// Add trees after all base terrain is placed to ensure proper layering
for (var i = 0; i < treePositions.length; i++) {
var pos = treePositions[i];
// First ensure there's normal terrain, then add tree on top
if (grid[pos.y][pos.x].terrainType !== 'normal') {
grid[pos.y][pos.x].addTerrain('normal');
}
// Add tree terrain graphics on top of normal terrain
var treeGraphics = grid[pos.y][pos.x].attachAsset('tree', {
anchorX: 0.5,
anchorY: 0.5
});
treeGraphics.alpha = 1.0;
// Set terrain type but keep the normal terrain graphics underneath
grid[pos.y][pos.x].terrainType = 'tree';
}
}
function placeUnit(unit, x, y) {
unit.gridX = x;
unit.gridY = y;
unit.x = grid[y][x].x;
unit.y = grid[y][x].y;
grid[y][x].unit = unit;
game.addChild(unit);
}
function clearHighlights() {
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x].hideHighlights();
}
}
}
function showMovementOptions(unit) {
// Use pathfinding to check reachable tiles considering terrain
var reachableTiles = getReachableTiles(unit);
for (var i = 0; i < reachableTiles.length; i++) {
var tile = reachableTiles[i];
grid[tile.y][tile.x].showHighlight();
}
}
function showAttackOptions(unit) {
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (unit.canAttack(x, y) && grid[y][x].unit && grid[y][x].unit.team !== unit.team && grid[y][x].unit.hp > 0) {
grid[y][x].showAttackRange();
}
}
}
}
function showHealOptions(unit) {
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (unit.canHeal(x, y) && grid[y][x].unit && grid[y][x].unit.team === unit.team && grid[y][x].unit.hp < grid[y][x].unit.maxHp) {
grid[y][x].showAttackRange();
}
}
}
}
function showAttackRangePreview(unit) {
var reachableTiles = getReachableTiles(unit);
var attackRange = unit.getRange();
var reachablePositions = {};
var attackOutlineTiles = {};
// Mark all reachable positions (blue movement range)
for (var i = 0; i < reachableTiles.length; i++) {
var tile = reachableTiles[i];
reachablePositions[tile.x + ',' + tile.y] = true;
// Show blue movement range
grid[tile.y][tile.x].showHighlight();
}
// Mark current position as reachable too
reachablePositions[unit.gridX + ',' + unit.gridY] = true;
// Calculate attack outline around the perimeter of movement range
var allMovementPositions = reachableTiles.slice();
allMovementPositions.push({
x: unit.gridX,
y: unit.gridY
});
// For each movement position, check outward for attack range
for (var i = 0; i < allMovementPositions.length; i++) {
var pos = allMovementPositions[i];
// Check all possible attack positions from this movement position
for (var dy = -attackRange; dy <= attackRange; dy++) {
for (var dx = -attackRange; dx <= attackRange; dx++) {
var distance = Math.abs(dx) + Math.abs(dy);
// Check if this distance is valid for the unit's attack pattern
var isValidAttackDistance = false;
if (unit.weaponType === 'bow') {
isValidAttackDistance = distance === 2; // Bow attacks only at exactly range 2, not range 1
} else if (unit.weaponType === 'manakete') {
isValidAttackDistance = distance >= 1 && distance <= 2; // Manakete attacks at range 1-2 like magic
} else if (unit.weaponType === 'healer') {
isValidAttackDistance = distance === 1; // Healer heals at range 1
} else {
isValidAttackDistance = distance >= 1 && distance <= attackRange; // Regular units attack from 1 to max range
}
if (isValidAttackDistance) {
var ax = pos.x + dx;
var ay = pos.y + dy;
if (ax >= 0 && ax < GRID_SIZE && ay >= 0 && ay < GRID_SIZE) {
var key = ax + ',' + ay;
// Only show attack outline if it's NOT a reachable movement position
if (!reachablePositions[key]) {
attackOutlineTiles[key] = {
x: ax,
y: ay,
isHeal: unit.weaponType === 'healer'
};
}
}
}
}
}
}
// Show attack outline tiles
for (var key in attackOutlineTiles) {
var pos = attackOutlineTiles[key];
if (pos.isHeal) {
grid[pos.y][pos.x].showHealRangePreview();
} else {
grid[pos.y][pos.x].showAttackRangePreview();
}
}
}
function getReachableTiles(unit) {
if (!unit) {
return [];
}
var reachableTiles = [];
var maxRange = unit.getMovementRange();
var visited = {};
var queue = [{
x: unit.gridX,
y: unit.gridY,
cost: 0
}];
visited[unit.gridX + ',' + unit.gridY] = true;
while (queue.length > 0) {
var current = queue.shift();
// Check all 4 directions
var directions = [{
x: 0,
y: 1
}, {
x: 0,
y: -1
}, {
x: 1,
y: 0
}, {
x: -1,
y: 0
}];
for (var d = 0; d < directions.length; d++) {
var dir = directions[d];
var newX = current.x + dir.x;
var newY = current.y + dir.y;
var key = newX + ',' + newY;
if (newX < 0 || newX >= GRID_SIZE || newY < 0 || newY >= GRID_SIZE) {
continue;
}
if (visited[key]) {
continue;
}
// Allow passing through same-team units during pathfinding, but not ending on them
var tileUnit = grid[newY][newX].unit;
if (tileUnit && tileUnit.team === unit.team) {
// Can pass through same-team units but cannot end movement here
// Continue pathfinding but mark this as a pass-through tile
} else if (tileUnit && tileUnit.team !== unit.team) {
// Cannot pass through enemy units
continue;
}
var moveCost = getMoveCost(unit, newX, newY);
if (moveCost === -1) {
continue;
} // Cannot move to this tile
var newCost = current.cost + moveCost;
if (newCost <= maxRange) {
visited[key] = true;
queue.push({
x: newX,
y: newY,
cost: newCost
});
if (newX !== unit.gridX || newY !== unit.gridY) {
reachableTiles.push({
x: newX,
y: newY,
cost: newCost
});
}
}
}
}
return reachableTiles;
}
function getMoveCost(unit, x, y) {
var tile = grid[y][x];
// Water - dragon riders and manaketes can cross
if (tile.terrainType === 'water' && unit.weaponType !== 'dragonRider' && unit.weaponType !== 'manakete') {
return -1; // Cannot move
}
// Tree - only dragon riders and manaketes can cross, all others cannot
if (tile.terrainType === 'tree') {
if (unit.weaponType === 'dragonRider' || unit.weaponType === 'manakete') {
return 1; // Normal cost for dragonRider and manakete
}
return -1; // Cannot move through trees for all other units
}
// Normal terrain and bridges
return 1;
}
function showEnemyMovementOptions(unit) {
var reachableTiles = getReachableTiles(unit);
for (var i = 0; i < reachableTiles.length; i++) {
var tile = reachableTiles[i];
grid[tile.y][tile.x].showHighlight();
}
}
function moveUnit(unit, targetX, targetY) {
// Check if target tile is occupied by another unit
if (grid[targetY][targetX].unit) {
// Find nearest valid empty tile
var validTile = findNearestValidTile(unit, targetX, targetY);
targetX = validTile.x;
targetY = validTile.y;
}
grid[unit.gridY][unit.gridX].unit = null;
unit.gridX = targetX;
unit.gridY = targetY;
grid[targetY][targetX].unit = unit;
tween(unit, {
x: grid[targetY][targetX].x,
y: grid[targetY][targetX].y
}, {
duration: 300
});
clearHighlights();
}
function isValidMove(unit, x, y) {
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
return false;
}
// Cannot end movement on a tile with any unit (same team or enemy)
if (grid[y][x].unit) {
return false;
}
var distance = Math.abs(unit.gridX - x) + Math.abs(unit.gridY - y);
if (distance > unit.getMovementRange()) {
return false;
}
// Check terrain restrictions
var targetTile = grid[y][x];
if (targetTile.terrainType === 'water') {
// Only dragon riders and manaketes can cross water
if (unit.weaponType !== 'dragonRider' && unit.weaponType !== 'manakete') {
return false;
}
}
return true;
}
function getTileFromPosition(screenX, screenY) {
var startX = (2048 - GRID_SIZE * TILE_SIZE) / 2;
var startY = (2732 - GRID_SIZE * TILE_SIZE) / 2;
var gridX = Math.floor((screenX - startX) / TILE_SIZE);
var gridY = Math.floor((screenY - startY) / TILE_SIZE);
if (gridX >= 0 && gridX < GRID_SIZE && gridY >= 0 && gridY < GRID_SIZE) {
return {
x: gridX,
y: gridY
};
}
return null;
}
// Enemy AI functions
function getValidMovesForUnit(unit) {
return getReachableTiles(unit);
}
function findNearestValidTile(unit, targetX, targetY) {
// If target tile is valid and empty, use it
if (targetX >= 0 && targetX < GRID_SIZE && targetY >= 0 && targetY < GRID_SIZE && !grid[targetY][targetX].unit && getMoveCost(unit, targetX, targetY) !== -1) {
return {
x: targetX,
y: targetY
};
}
// Search in expanding circles for nearest valid empty tile
for (var radius = 1; radius <= GRID_SIZE; radius++) {
for (var dy = -radius; dy <= radius; dy++) {
for (var dx = -radius; dx <= radius; dx++) {
// Only check tiles at current radius (perimeter of circle)
if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue;
var newX = targetX + dx;
var newY = targetY + dy;
if (newX >= 0 && newX < GRID_SIZE && newY >= 0 && newY < GRID_SIZE && !grid[newY][newX].unit && getMoveCost(unit, newX, newY) !== -1) {
return {
x: newX,
y: newY
};
}
}
}
}
// If no valid tile found, return current position
return {
x: unit.gridX,
y: unit.gridY
};
}
function updateUnitCounters() {
var alivePlayerUnits = 0;
var aliveEnemyUnits = 0;
// Count alive player units
for (var i = 0; i < playerUnits.length; i++) {
if (playerUnits[i] && playerUnits[i].hp > 0) {
alivePlayerUnits++;
}
}
// Count alive enemy units
for (var i = 0; i < enemyUnits.length; i++) {
if (enemyUnits[i] && enemyUnits[i].hp > 0) {
aliveEnemyUnits++;
}
}
// Update counter display
playerCounter.setText(alivePlayerUnits + '/10');
enemyCounter.setText(aliveEnemyUnits + '/10');
}
function findAttackTargets(unit) {
var targets = [];
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (unit.canAttack(x, y) && grid[y][x].unit && grid[y][x].unit.team !== unit.team && grid[y][x].unit.hp > 0) {
targets.push({
unit: grid[y][x].unit,
x: x,
y: y
});
}
}
}
return targets;
}
function checkAutoEndTurn() {
if (currentTurn !== 'player') {
return;
}
var allMoved = true;
for (var i = 0; i < playerUnits.length; i++) {
if (!playerUnits[i].hasMoved && playerUnits[i].hp > 0) {
allMoved = false;
break;
}
}
if (allMoved) {
// Auto end turn
LK.setTimeout(function () {
for (var i = 0; i < playerUnits.length; i++) {
playerUnits[i].resetTurn();
}
currentTurn = 'enemy';
turnText.setText('Enemy Turn');
phaseText.setText('Enemy AI thinking...');
LK.setTimeout(executeEnemyAI, 1000);
}, 500);
}
}
function executeEnemyAI() {
if (currentTurn !== 'enemy') {
return;
}
// Hide end turn button during enemy turn
endTurnBtn.alpha = 0;
endTurnBtn.interactive = false;
var currentEnemyIndex = 0;
function processNextEnemy() {
if (currentEnemyIndex >= enemyUnits.length) {
// All enemies processed, end turn
for (var i = 0; i < enemyUnits.length; i++) {
enemyUnits[i].resetTurn();
}
currentTurn = 'player';
turnText.setText('Player Turn');
selectedUnit = null;
selectedEnemyUnit = null;
turnPhase = 'select';
phaseText.setText('Select Unit');
clearHighlights();
removeActionButtons();
// Show end turn button again for player turn
endTurnBtn.alpha = 1;
endTurnBtn.interactive = true;
return;
}
var enemy = enemyUnits[currentEnemyIndex];
currentEnemyIndex++;
if (enemy.hasMoved || enemy.hp <= 0) {
processNextEnemy();
return;
}
// Check if enemy can attack from current position
var currentTargets = findAttackTargets(enemy);
if (currentTargets.length > 0) {
// Attack the first available target
var target = currentTargets[0].unit;
// Add attack shake animation
var originalX = enemy.x;
tween(enemy, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX
}, {
duration: 100
});
}
});
}
});
LK.getSound('Impacts').play();
showAttackEffect(target);
var isDead = target.takeDamage(enemy.attack, enemy.weaponType);
// Check for counterattack if target survived
if (!isDead) {
var attackerDied = target.counterAttack(enemy);
if (attackerDied) {
grid[enemy.gridY][enemy.gridX].unit = null;
LK.getSound('Vanish').play();
tween(enemy, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
enemy.destroy();
}
});
var index = enemyUnits.indexOf(enemy);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
if (enemy.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
return;
}
}
if (isDead) {
grid[target.gridY][target.gridX].unit = null;
LK.getSound('Vanish').play();
tween(target, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
target.destroy();
}
});
if (target.team === 'player') {
var index = playerUnits.indexOf(target);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
// Check if player commander was defeated
if (target.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
}
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
return;
}
// Try to move closer to player units
var validMoves = getValidMovesForUnit(enemy);
if (validMoves.length > 0) {
// Find the move that gets closest to any player unit
var bestMove = null;
var bestScore = Infinity;
for (var m = 0; m < validMoves.length; m++) {
var move = validMoves[m];
var minDistanceToPlayer = Infinity;
for (var p = 0; p < playerUnits.length; p++) {
if (playerUnits[p].hp <= 0) {
continue;
}
var dist = Math.abs(move.x - playerUnits[p].gridX) + Math.abs(move.y - playerUnits[p].gridY);
minDistanceToPlayer = Math.min(minDistanceToPlayer, dist);
}
if (minDistanceToPlayer < bestScore) {
bestScore = minDistanceToPlayer;
bestMove = move;
}
}
if (bestMove) {
// Find nearest valid tile if bestMove position is occupied
var finalMove = findNearestValidTile(enemy, bestMove.x, bestMove.y);
moveUnit(enemy, finalMove.x, finalMove.y);
LK.setTimeout(function () {
// Check if can attack after moving
var targetsAfterMove = findAttackTargets(enemy);
if (targetsAfterMove.length > 0) {
var target = targetsAfterMove[0].unit;
// Add attack shake animation
var originalX = enemy.x;
tween(enemy, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX
}, {
duration: 100
});
}
});
}
});
LK.getSound('Impacts').play();
showAttackEffect(target);
var isDead = target.takeDamage(enemy.attack, enemy.weaponType);
// Check for counterattack if target survived
if (!isDead) {
var attackerDied = target.counterAttack(enemy);
if (attackerDied) {
grid[enemy.gridY][enemy.gridX].unit = null;
LK.getSound('Vanish').play();
tween(enemy, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
enemy.destroy();
}
});
var index = enemyUnits.indexOf(enemy);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
if (enemy.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
return;
}
}
if (isDead) {
grid[target.gridY][target.gridX].unit = null;
LK.getSound('Vanish').play();
tween(target, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
target.destroy();
}
});
if (target.team === 'player') {
var index = playerUnits.indexOf(target);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
// Check if player commander was defeated
if (target.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
}
}
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
}, 400);
return;
}
}
// No valid moves, end this unit's turn
enemy.endTurn();
LK.setTimeout(processNextEnemy, 300);
}
processNextEnemy();
}
// End turn button
endTurnBtn.interactive = true;
endTurnBtn.down = function () {
if (gameState !== 'playing') {
return;
}
if (currentTurn === 'player') {
for (var i = 0; i < playerUnits.length; i++) {
playerUnits[i].resetTurn();
}
currentTurn = 'enemy';
turnText.setText('Enemy Turn');
phaseText.setText('Enemy AI thinking...');
LK.setTimeout(executeEnemyAI, 1000);
} else {
for (var i = 0; i < enemyUnits.length; i++) {
enemyUnits[i].resetTurn();
}
currentTurn = 'player';
turnText.setText('Player Turn');
}
selectedUnit = null;
selectedEnemyUnit = null;
originalUnitPosition = null;
turnPhase = 'select';
phaseText.setText('Select Unit');
clearHighlights();
removeActionButtons();
};
// Logo and follow text variables
var logoSprite = null;
var followText = null;
// Create and show logo and follow text on menu
logoSprite = LK.getAsset('Logo', {
anchorX: 0,
anchorY: 0,
scaleX: 1.2,
scaleY: 1.2
});
logoSprite.x = 120;
logoSprite.y = 20;
LK.gui.topLeft.addChild(logoSprite);
followText = new Text2('FOLLOW ME: @Mugiwara_Shanks', {
size: 28,
fill: 0xFFFFFF
});
followText.anchor.set(0, 0);
followText.x = 250;
followText.y = 50;
LK.gui.topLeft.addChild(followText);
// Play main menu music when the game starts
LK.playMusic('MainMenu');
// Menu click handler - start game when clicking anywhere on menu
game.down = function (x, y, obj) {
// Only handle menu clicks when in menu state
if (gameState === 'menu') {
// Stop main menu music when starting the game
LK.stopMusic();
startGame();
return;
}
// Only handle game clicks when in playing state
if (gameState !== 'playing') {
return;
}
var tilePos = getTileFromPosition(x, y);
if (!tilePos) {
return;
}
var clickedTile = grid[tilePos.y][tilePos.x];
if (turnPhase === 'select') {
// Handle enemy unit selection for viewing movement
if (clickedTile.unit && clickedTile.unit.team !== currentTurn) {
if (selectedEnemyUnit === clickedTile.unit) {
// Clicking same enemy again - hide movement
selectedEnemyUnit = null;
clearHighlights();
hideUnitInfo();
} else {
// Clicking different enemy - show movement and attack range
selectedEnemyUnit = clickedTile.unit;
clearHighlights();
showEnemyMovementOptions(selectedEnemyUnit);
showAttackRangePreview(selectedEnemyUnit);
showUnitInfo(selectedEnemyUnit);
}
return;
}
// Clear enemy selection when clicking elsewhere
if (selectedEnemyUnit) {
selectedEnemyUnit = null;
clearHighlights();
}
// Handle player unit selection - allow viewing stats of moved units
if (clickedTile.unit && clickedTile.unit.team === currentTurn) {
// If unit has already moved, just show info and return
if (clickedTile.unit.hasMoved) {
showUnitInfo(clickedTile.unit);
return;
}
selectedUnit = clickedTile.unit;
originalUnitPosition = {
x: selectedUnit.gridX,
y: selectedUnit.gridY
};
// Show unit info
showUnitInfo(selectedUnit);
// Check for attack targets from current position on first click
var hasAttackTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canAttack(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team !== selectedUnit.team && grid[ty][tx].unit.hp > 0) {
hasAttackTargets = true;
break;
}
}
if (hasAttackTargets) {
break;
}
}
if (hasAttackTargets) {
clearHighlights();
// Create move button
var moveBtn = new Text2('Move', {
size: 40,
fill: 0x00FF00
});
moveBtn.anchor.set(0.5, 1);
moveBtn.y = -120;
LK.gui.bottom.addChild(moveBtn);
moveBtn.interactive = true;
moveBtn.down = function () {
// Remove action buttons and continue with move
LK.gui.bottom.removeChild(moveBtn);
if (attackBtn) {
LK.gui.bottom.removeChild(attackBtn);
attackBtn = null;
}
if (ignoreBtn) {
LK.gui.bottom.removeChild(ignoreBtn);
ignoreBtn = null;
}
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
};
// Show attack and ignore buttons
turnPhase = 'chooseAction';
phaseText.setText('Attack, Ignore or Move');
showActionButtons();
} else {
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
showAttackRangePreview(selectedUnit);
}
}
} else if (turnPhase === 'move') {
// Cancel move selection by clicking anywhere
if (!isValidMove(selectedUnit, tilePos.x, tilePos.y)) {
selectedUnit = null;
originalUnitPosition = null;
turnPhase = 'select';
phaseText.setText('Select Unit');
clearHighlights();
hideUnitInfo();
return;
}
if (isValidMove(selectedUnit, tilePos.x, tilePos.y)) {
moveUnit(selectedUnit, tilePos.x, tilePos.y);
LK.setTimeout(function () {
if (!selectedUnit) {
return;
}
if (selectedUnit.weaponType === 'healer') {
var hasHealTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canHeal(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team === selectedUnit.team && grid[ty][tx].unit.hp < grid[ty][tx].unit.maxHp) {
hasHealTargets = true;
break;
}
}
if (hasHealTargets) {
break;
}
}
if (hasHealTargets) {
turnPhase = 'chooseAction';
phaseText.setText('Heal or Ignore');
showActionButtons();
} else {
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
}
} else {
var hasAttackTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canAttack(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team !== selectedUnit.team && grid[ty][tx].unit.hp > 0) {
hasAttackTargets = true;
break;
}
}
if (hasAttackTargets) {
break;
}
}
if (hasAttackTargets) {
turnPhase = 'chooseAction';
phaseText.setText('Attack or Ignore');
showActionButtons();
} else {
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
}
}
}, 350);
}
} else if (turnPhase === 'heal') {
if (clickedTile.unit && clickedTile.unit.team === selectedUnit.team && selectedUnit.canHeal(tilePos.x, tilePos.y)) {
var target = clickedTile.unit;
selectedUnit.heal(target);
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
} else {
// Clicked elsewhere during heal - return to original position
if (originalUnitPosition && selectedUnit) {
moveUnit(selectedUnit, originalUnitPosition.x, originalUnitPosition.y);
LK.setTimeout(function () {
if (selectedUnit) {
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
}
}, 350);
}
}
} else if (turnPhase === 'attack') {
if (clickedTile.unit && clickedTile.unit.team !== selectedUnit.team && selectedUnit.canAttack(tilePos.x, tilePos.y)) {
var target = clickedTile.unit;
// Add attack shake animation
if (!selectedUnit) {
return;
}
var originalX = selectedUnit.x;
tween(selectedUnit, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
if (!selectedUnit) {
return;
}
tween(selectedUnit, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
if (!selectedUnit) {
return;
}
tween(selectedUnit, {
x: originalX
}, {
duration: 100
});
}
});
}
});
LK.getSound('Impacts').play();
showAttackEffect(target);
var isDead = target.takeDamage(selectedUnit.attack, selectedUnit.weaponType);
// Check for counterattack if target survived
if (!isDead) {
var attackerDied = target.counterAttack(selectedUnit);
if (attackerDied) {
grid[selectedUnit.gridY][selectedUnit.gridX].unit = null;
LK.getSound('Vanish').play();
var dyingUnit = selectedUnit; // Store reference before selectedUnit becomes null
tween(selectedUnit, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (dyingUnit) {
dyingUnit.destroy();
}
}
});
if (selectedUnit.team === 'player') {
var index = playerUnits.indexOf(selectedUnit);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
if (selectedUnit.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
} else {
var index = enemyUnits.indexOf(selectedUnit);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
if (selectedUnit.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
}
}
}
if (isDead) {
grid[target.gridY][target.gridX].unit = null;
LK.getSound('Vanish').play();
tween(target, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
target.destroy();
}
});
if (target.team === 'player') {
var index = playerUnits.indexOf(target);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
// Check if player commander was defeated
if (target.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
} else {
var index = enemyUnits.indexOf(target);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
// Check if enemy commander was defeated
if (target.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
}
}
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
} else {
// Clicked elsewhere during attack - return to original position
if (originalUnitPosition && selectedUnit) {
moveUnit(selectedUnit, originalUnitPosition.x, originalUnitPosition.y);
LK.setTimeout(function () {
if (selectedUnit) {
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
}
}, 350);
}
}
}
};
function showAttackEffect(target) {
var attackEffect = LK.getAsset('attackEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
attackEffect.x = target.x;
attackEffect.y = target.y;
game.addChild(attackEffect);
// Flash effect - fade in quickly then fade out
tween(attackEffect, {
alpha: 0.8,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 150,
onFinish: function onFinish() {
tween(attackEffect, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
onFinish: function onFinish() {
game.removeChild(attackEffect);
}
});
}
});
}
function showHealEffect(target) {
var healEffect = LK.getAsset('healEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
healEffect.x = target.x;
healEffect.y = target.y;
healEffect.tint = 0x00FF00; // Green tint for healing
game.addChild(healEffect);
// Flash effect - fade in quickly then fade out
tween(healEffect, {
alpha: 0.8,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 150,
onFinish: function onFinish() {
tween(healEffect, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
onFinish: function onFinish() {
game.removeChild(healEffect);
}
});
}
});
}
function startGame() {
// Hide logo and follow text when starting game
if (logoSprite) {
LK.gui.topLeft.removeChild(logoSprite);
logoSprite = null;
}
if (followText) {
LK.gui.topLeft.removeChild(followText);
followText = null;
}
// Show title and objective message first
var titleText = new Text2('ICE EMBLEM', {
size: 120,
fill: 0x00BFFF
});
titleText.anchor.set(0.5, 1);
titleText.y = -200;
LK.gui.bottom.addChild(titleText);
var objectiveText = new Text2('DEFEAT THE COMMANDER', {
size: 100,
fill: 0xFFD700
});
objectiveText.anchor.set(0.5, 1);
objectiveText.y = -100;
LK.gui.bottom.addChild(objectiveText);
// Fade in title and objective text together
tween(titleText, {
alpha: 1
}, {
duration: 1000
});
// Fade in and then fade out the objective text
tween(objectiveText, {
alpha: 1
}, {
duration: 1000,
onFinish: function onFinish() {
LK.setTimeout(function () {
// Fade out both title and objective text
tween(titleText, {
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
LK.gui.bottom.removeChild(titleText);
}
});
tween(objectiveText, {
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
LK.gui.bottom.removeChild(objectiveText);
}
});
}, 2000);
}
});
LK.setTimeout(function () {
// Show game UI after objective text fades out
turnText.alpha = 1;
phaseText.alpha = 1;
playerCounter.alpha = 1;
enemyCounter.alpha = 1;
endTurnBtn.alpha = 1;
endTurnBtn.interactive = true;
unitInfoPanel.visible = true;
// Change game state
gameState = 'playing';
// Initialize and start game
initializeGrid();
initializeUnits();
updateUnitCounters();
// Play background music
LK.playMusic('MedievalMusicFantasy1');
}, 4000);
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var GridTile = Container.expand(function (gridX, gridY) {
var self = Container.call(this);
self.gridX = gridX;
self.gridY = gridY;
self.unit = null;
self.hasTerrain = false;
var tileGraphics = self.attachAsset('gridTile', {
anchorX: 0.5,
anchorY: 0.5
});
tileGraphics.alpha = 0; // Make gridTile transparent so terrain is visible
self.terrainType = 'normal'; // normal, tree, water, bridge
self.terrainGraphics = null; // Store terrain graphics reference
self.addTerrain = function (terrainType) {
// Remove existing terrain graphics if present
if (self.terrainGraphics) {
self.removeChild(self.terrainGraphics);
self.terrainGraphics = null;
}
self.terrainType = terrainType || 'normal';
var assetName = 'terrain';
if (terrainType === 'tree') {
assetName = 'tree';
} else if (terrainType === 'water') {
assetName = 'water';
} else if (terrainType === 'bridge') {
assetName = 'bridge';
} else if (terrainType === 'normal') {
assetName = 'terrain';
}
self.terrainGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// For trees, make them fully opaque and visible, for other terrain make slightly transparent
if (terrainType === 'tree') {
self.terrainGraphics.alpha = 1.0;
} else {
self.terrainGraphics.alpha = 0.8;
}
// Move terrain graphics to bottom of display list so highlights show on top
self.setChildIndex(self.terrainGraphics, 0);
self.hasTerrain = true;
};
// Add highlight graphics AFTER terrain so they appear on top
var highlightGraphics = self.attachAsset('highlightTile', {
anchorX: 0.5,
anchorY: 0.5
});
highlightGraphics.alpha = 0;
var attackGraphics = self.attachAsset('attackTile', {
anchorX: 0.5,
anchorY: 0.5
});
attackGraphics.alpha = 0;
var attackRangeGraphics = self.attachAsset('attackRangeTile', {
anchorX: 0.5,
anchorY: 0.5
});
attackRangeGraphics.alpha = 0;
var healRangeGraphics = self.attachAsset('healRangeTile', {
anchorX: 0.5,
anchorY: 0.5
});
healRangeGraphics.alpha = 0;
self.showHighlight = function () {
highlightGraphics.alpha = 0.4;
attackGraphics.alpha = 0;
};
self.showAttackRange = function () {
attackGraphics.alpha = 0.5;
highlightGraphics.alpha = 0;
};
self.showAttackRangePreview = function () {
attackRangeGraphics.alpha = 0.3;
};
self.showHealRangePreview = function () {
healRangeGraphics.alpha = 0.3;
};
self.hideHighlights = function () {
highlightGraphics.alpha = 0;
attackGraphics.alpha = 0;
attackRangeGraphics.alpha = 0;
healRangeGraphics.alpha = 0;
};
return self;
});
var Unit = Container.expand(function (type, team, weaponType, swordVariant, unitName) {
var self = Container.call(this);
self.type = type;
self.team = team;
self.weaponType = weaponType;
self.swordVariant = swordVariant || 1; // Default to sword1 if not specified
self.name = unitName || 'Unknown';
self.gridX = 0;
self.gridY = 0;
// Set stats based on unit name and type
if (unitName === 'Ardan') {
self.hp = 40;
self.attack = 19;
} else if (unitName === 'Kael') {
self.hp = 28;
self.attack = 13;
} else if (unitName === 'Hikari') {
self.hp = 32;
self.attack = 14;
} else if (unitName === 'Brutus') {
self.hp = 52;
self.attack = 15;
} else if (unitName === 'Mizuki') {
self.hp = 34;
self.attack = 12;
} else if (unitName === 'Lyra') {
self.hp = 35;
self.attack = 16;
} else if (unitName === 'Darius') {
self.hp = 34;
self.attack = 16;
} else if (unitName === 'Eirynn') {
self.hp = 26;
self.attack = 10; // Healer uses attack stat for healing power
} else if (unitName === 'Elian') {
self.hp = 27;
self.attack = 13;
} else if (unitName === 'Irios') {
self.hp = 30;
self.attack = 17;
} else {
// Default stats for unnamed units
self.hp = 2;
self.attack = 1;
}
self.maxHp = self.hp;
self.defense = 1;
self.hasMoved = false;
self.hasAttacked = false;
var unitGraphics;
if (type === 'commander') {
unitGraphics = self.attachAsset('commander', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'rider') {
unitGraphics = self.attachAsset('rider', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'dragonRider') {
unitGraphics = self.attachAsset('dragonRider', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'healer') {
unitGraphics = self.attachAsset('healer', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'armored') {
unitGraphics = self.attachAsset('armored', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'manakete') {
unitGraphics = self.attachAsset('manakete', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
// Use weapon type as sprite
if (weaponType === 'sword') {
var swordAsset = 'sword' + self.swordVariant; // Use sword1 or sword2
unitGraphics = self.attachAsset(swordAsset, {
anchorX: 0.5,
anchorY: 0.5
});
} else if (weaponType === 'bow') {
unitGraphics = self.attachAsset('bow', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (weaponType === 'magic') {
unitGraphics = self.attachAsset('magic', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
var swordAsset = 'sword' + (self.swordVariant || 1); // Default to sword1
unitGraphics = self.attachAsset(swordAsset, {
anchorX: 0.5,
anchorY: 0.5
});
}
}
// Add team color filter directly to unit graphics (more opaque, no background)
if (team === 'player') {
unitGraphics.tint = 0x5555FF; // Blue tint for player units
} else {
unitGraphics.tint = 0xFF5555; // Red tint for enemy units
}
// Add commander crown
if (type === 'commander') {
var crown = new Text2('♛', {
size: 25,
fill: 0xFFD700
});
crown.anchor.set(1, 1);
crown.x = 50;
crown.y = 50;
self.addChild(crown);
}
self.updateHpDisplay = function () {
if (self.hp <= 0) {
unitGraphics.alpha = 0.3;
}
};
self.takeDamage = function (damage, attackerWeaponType) {
var baseDamage = damage; // Use the actual attack stat passed as parameter
// Apply specific weapon advantage bonuses
if (attackerWeaponType === 'bow' && self.type === 'dragonRider') {
baseDamage = damage + 1; // Bow does +1 bonus damage to Dragon Rider
} else if (attackerWeaponType === 'magic' && self.type === 'armored') {
baseDamage = damage + 1; // Magic does +1 bonus damage to Armored
}
// Calculate final damage after defense
var actualDamage = Math.max(1, baseDamage - self.defense);
if (grid[self.gridY][self.gridX].hasTerrain) {
actualDamage = Math.max(1, actualDamage - 1);
}
self.hp -= actualDamage;
self.updateHpDisplay();
return self.hp <= 0;
};
self.getRange = function () {
if (self.weaponType === 'sword') {
return 1;
}
if (self.weaponType === 'bow') {
return 2;
}
if (self.weaponType === 'magic') {
return 2;
}
if (self.weaponType === 'rider') {
return 1;
}
if (self.weaponType === 'dragonRider') {
return 1;
}
if (self.weaponType === 'healer') {
return 1;
}
if (self.weaponType === 'armored') {
return 2;
}
return 1;
};
self.getMovementRange = function () {
if (self.weaponType === 'rider' || self.weaponType === 'dragonRider') {
return 5;
}
if (self.weaponType === 'manakete') {
return 4;
}
if (self.weaponType === 'armored') {
return 3;
}
return 4; // Default movement for sword, bow, magic, healer
};
self.canAttack = function (targetX, targetY) {
var distance = Math.abs(self.gridX - targetX) + Math.abs(self.gridY - targetY);
if (self.weaponType === 'healer') {
return false;
} // Healer cannot attack
if (self.weaponType === 'bow') {
return distance === 2; // Bow can only attack at exactly range 2, not adjacent
}
if (self.weaponType === 'manakete') {
return distance >= 1 && distance <= 2; // Manakete can attack at range 1-2 like magic
}
return distance <= self.getRange();
};
self.canHeal = function (targetX, targetY) {
if (self.weaponType !== 'healer') {
return false;
}
var distance = Math.abs(self.gridX - targetX) + Math.abs(self.gridY - targetY);
return distance === 1; // Healer can heal adjacent allies
};
self.heal = function (target) {
if (target.hp < target.maxHp) {
target.hp += 10;
if (target.hp > target.maxHp) {
target.hp = target.maxHp;
}
target.updateHpDisplay();
LK.getSound('HealSound').play();
showHealEffect(target);
return true;
}
return false;
};
self.resetTurn = function () {
self.hasMoved = false;
self.hasAttacked = false;
unitGraphics.alpha = 1.0;
// Restore original team color
if (self.team === 'player') {
unitGraphics.tint = 0x5555FF; // Blue tint for player units
} else {
unitGraphics.tint = 0xFF5555; // Red tint for enemy units
}
};
self.counterAttack = function (attacker) {
if (self.hp <= 0) return false; // Can't counterattack if dead
if (!self.canAttack(attacker.gridX, attacker.gridY)) return false; // Must be in range
// Add counterattack shake animation
var originalX = self.x;
tween(self, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(self, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(self, {
x: originalX
}, {
duration: 100
});
}
});
}
});
showAttackEffect(attacker);
var isDead = attacker.takeDamage(self.attack, self.weaponType);
return isDead;
};
self.endTurn = function () {
self.hasMoved = true;
self.hasAttacked = true;
unitGraphics.alpha = 0.6;
// Add gray tint for moved allied units
if (self.team === 'player') {
unitGraphics.tint = 0x888888; // Gray tint for moved player units
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2d2d2d
});
/****
* Game Code
****/
// Add background field
var backgroundField = LK.getAsset('field', {
anchorX: 0,
anchorY: 0,
alpha: 0.7
});
backgroundField.x = 0;
backgroundField.y = 0;
game.addChild(backgroundField);
var GRID_SIZE = 15;
var TILE_SIZE = 128;
var grid = [];
var playerUnits = [];
var enemyUnits = [];
var selectedUnit = null;
var originalUnitPosition = null; // Store original position when moving
var selectedEnemyUnit = null; // Track selected enemy for movement display
var currentTurn = 'player';
var gameState = 'menu'; // Start in menu state instead of playing
var turnPhase = 'select'; // 'select', 'move', 'chooseAction', 'attack'
// UI
var turnText = new Text2('Player Turn', {
size: 60,
fill: 0xFFFFFF
});
turnText.anchor.set(0.5, 0);
turnText.alpha = 0; // Hidden initially
LK.gui.top.addChild(turnText);
var phaseText = new Text2('Select Unit', {
size: 40,
fill: 0xFFFFFF
});
phaseText.anchor.set(0.5, 0);
phaseText.y = 80;
phaseText.alpha = 0; // Hidden initially
LK.gui.top.addChild(phaseText);
// Unit counters
var playerCounter = new Text2('10/10', {
size: 35,
fill: 0x5555FF
});
playerCounter.anchor.set(0, 1);
playerCounter.x = 20;
playerCounter.y = -120;
playerCounter.alpha = 0; // Hidden initially
LK.gui.bottomLeft.addChild(playerCounter);
var enemyCounter = new Text2('10/10', {
size: 35,
fill: 0xFF5555
});
enemyCounter.anchor.set(1, 1);
enemyCounter.x = -20;
enemyCounter.y = -120;
enemyCounter.alpha = 0; // Hidden initially
LK.gui.bottomRight.addChild(enemyCounter);
var endTurnBtn = new Text2('End Turn', {
size: 50,
fill: 0x00FF00
});
endTurnBtn.anchor.set(0.5, 1);
endTurnBtn.alpha = 0; // Hidden initially
endTurnBtn.interactive = false; // Disabled initially
LK.gui.bottom.addChild(endTurnBtn);
// Unit info panel
var unitInfoPanel = new Container();
unitInfoPanel.x = -20;
unitInfoPanel.y = 120;
LK.gui.topRight.addChild(unitInfoPanel);
var unitInfoBg = LK.getAsset('terrain', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.5,
scaleY: 2.2
});
unitInfoBg.tint = 0x444444;
unitInfoBg.alpha = 0.8;
unitInfoPanel.addChild(unitInfoBg);
var unitInfoImage = null;
var unitInfoName = new Text2('', {
size: 32,
fill: 0xFFD700
});
unitInfoName.anchor.set(1, 0);
unitInfoName.x = -20;
unitInfoName.y = -90;
unitInfoPanel.addChild(unitInfoName);
var unitInfoClass = new Text2('', {
size: 30,
fill: 0xFFFFFF
});
unitInfoClass.anchor.set(1, 0);
unitInfoClass.x = -20;
unitInfoClass.y = -60;
unitInfoPanel.addChild(unitInfoClass);
var unitInfoHP = new Text2('', {
size: 25,
fill: 0x00FF00
});
unitInfoHP.anchor.set(1, 0);
unitInfoHP.x = -20;
unitInfoHP.y = -25;
unitInfoPanel.addChild(unitInfoHP);
var unitInfoDamage = new Text2('', {
size: 25,
fill: 0xFF4444
});
unitInfoDamage.anchor.set(1, 0);
unitInfoDamage.x = -20;
unitInfoDamage.y = 10;
unitInfoPanel.addChild(unitInfoDamage);
unitInfoPanel.alpha = 0;
unitInfoPanel.visible = false; // Hide completely initially
function showUnitInfo(unit) {
if (unitInfoImage) {
unitInfoPanel.removeChild(unitInfoImage);
unitInfoImage = null;
}
var assetName = 'sword';
if (unit.type === 'commander') {
assetName = 'commander';
} else if (unit.type === 'rider') {
assetName = 'rider';
} else if (unit.type === 'dragonRider') {
assetName = 'dragonRider';
} else if (unit.type === 'healer') {
assetName = 'healer';
} else if (unit.type === 'armored') {
assetName = 'armored';
} else if (unit.type === 'manakete') {
assetName = 'manakete';
} else if (unit.weaponType === 'sword') {
assetName = 'sword' + unit.swordVariant; // Use sword1 or sword2
} else if (unit.weaponType === 'bow') {
assetName = 'bow';
} else if (unit.weaponType === 'magic') {
assetName = 'magic';
}
unitInfoImage = LK.getAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
var className = unit.type === 'commander' ? 'Commander' : unit.type === 'rider' ? 'Rider' : unit.type === 'dragonRider' ? 'Dragon Rider' : unit.type === 'healer' ? 'Healer' : unit.type === 'armored' ? 'Armored' : unit.type === 'manakete' ? 'Manakete' : unit.weaponType === 'sword' ? 'Sword' : unit.weaponType === 'bow' ? 'Bow' : unit.weaponType === 'magic' ? 'Magic' : 'Hero';
// Position image further left based on class name length to prevent overlap
var imageOffset = -220 - className.length * 8;
unitInfoImage.x = imageOffset;
unitInfoImage.y = -20;
// Remove tint from unit info image
unitInfoImage.tint = 0xFFFFFF;
unitInfoPanel.addChild(unitInfoImage);
unitInfoName.setText(unit.name);
unitInfoClass.setText(className);
unitInfoHP.setText('HP: ' + unit.hp + '/' + unit.maxHp);
if (unit.weaponType === 'healer') {
unitInfoDamage.setText('HEAL: ' + unit.attack);
} else {
unitInfoDamage.setText('ATK: ' + unit.attack);
}
unitInfoPanel.alpha = 1;
}
function hideUnitInfo() {
unitInfoPanel.alpha = 0;
}
// botones dinámicos de acción
var attackBtn = null;
var ignoreBtn = null;
// Action button variables
var danceBtn = null;
function showActionButtons() {
// Hide end turn button when showing action buttons
endTurnBtn.alpha = 0;
endTurnBtn.interactive = false;
if (selectedUnit.weaponType === 'healer') {
// Check if there are valid heal targets
var hasHealTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canHeal(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team === selectedUnit.team && grid[ty][tx].unit.hp < grid[ty][tx].unit.maxHp) {
hasHealTargets = true;
break;
}
}
if (hasHealTargets) {
break;
}
}
if (hasHealTargets) {
attackBtn = new Text2('Heal', {
size: 50,
fill: 0x00FF00
});
attackBtn.anchor.set(0.5, 1);
attackBtn.y = -60;
LK.gui.bottom.addChild(attackBtn);
attackBtn.interactive = true;
attackBtn.down = function () {
turnPhase = 'heal';
phaseText.setText('Choose ally to heal');
showHealOptions(selectedUnit);
removeActionButtons();
};
}
} else {
attackBtn = new Text2('Attack', {
size: 50,
fill: 0xFF0000
});
attackBtn.anchor.set(0.5, 1);
attackBtn.y = -60;
LK.gui.bottom.addChild(attackBtn);
attackBtn.interactive = true;
attackBtn.down = function () {
turnPhase = 'attack';
phaseText.setText('Choose enemy to attack');
showAttackOptions(selectedUnit);
removeActionButtons();
};
}
ignoreBtn = new Text2('Ignore', {
size: 50,
fill: 0xFFFF00
});
ignoreBtn.anchor.set(0.5, 1);
ignoreBtn.y = -10;
LK.gui.bottom.addChild(ignoreBtn);
ignoreBtn.interactive = true;
ignoreBtn.down = function () {
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
removeActionButtons();
hideUnitInfo();
// Check if all player units have moved
checkAutoEndTurn();
};
}
function removeActionButtons() {
// Show end turn button again
if (currentTurn === 'player') {
endTurnBtn.alpha = 1;
endTurnBtn.interactive = true;
}
if (attackBtn) {
LK.gui.bottom.removeChild(attackBtn);
attackBtn = null;
}
if (ignoreBtn) {
LK.gui.bottom.removeChild(ignoreBtn);
ignoreBtn = null;
}
}
// Initialize grid
function initializeGrid() {
var startX = (2048 - GRID_SIZE * TILE_SIZE) / 2;
var startY = (2732 - GRID_SIZE * TILE_SIZE) / 2;
for (var y = 0; y < GRID_SIZE; y++) {
grid[y] = [];
for (var x = 0; x < GRID_SIZE; x++) {
var tile = new GridTile(x, y);
tile.x = startX + x * TILE_SIZE + TILE_SIZE / 2;
tile.y = startY + y * TILE_SIZE + TILE_SIZE / 2;
grid[y][x] = tile;
game.addChild(tile);
}
}
// Generate terrain
generateTerrain();
// Add border frame around the map
var borderThickness = 20;
var mapStartX = (2048 - GRID_SIZE * TILE_SIZE) / 2;
var mapStartY = (2732 - GRID_SIZE * TILE_SIZE) / 2;
var mapWidth = GRID_SIZE * TILE_SIZE;
var mapHeight = GRID_SIZE * TILE_SIZE;
// Create border graphics using terrain assets
// Top border
var topBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: (mapWidth + borderThickness * 2) / 132,
scaleY: borderThickness / 132
});
topBorder.x = mapStartX - borderThickness;
topBorder.y = mapStartY - borderThickness;
topBorder.tint = 0x8B4513;
game.addChild(topBorder);
// Bottom border
var bottomBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: (mapWidth + borderThickness * 2) / 132,
scaleY: borderThickness / 132
});
bottomBorder.x = mapStartX - borderThickness;
bottomBorder.y = mapStartY + mapHeight;
bottomBorder.tint = 0x8B4513;
game.addChild(bottomBorder);
// Left border
var leftBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: borderThickness / 132,
scaleY: mapHeight / 132
});
leftBorder.x = mapStartX - borderThickness;
leftBorder.y = mapStartY;
leftBorder.tint = 0x8B4513;
game.addChild(leftBorder);
// Right border
var rightBorder = LK.getAsset('terrain', {
anchorX: 0,
anchorY: 0,
scaleX: borderThickness / 132,
scaleY: mapHeight / 132
});
rightBorder.x = mapStartX + mapWidth;
rightBorder.y = mapStartY;
rightBorder.tint = 0x8B4513;
game.addChild(rightBorder);
}
// Initialize units
function initializeUnits() {
// Player units (bottom-right corner)
var playerCommander = new Unit('commander', 'player', 'sword', 1, 'Ardan');
placeUnit(playerCommander, 12, 14);
playerUnits.push(playerCommander);
var playerSword1 = new Unit('hero', 'player', 'sword', 1, 'Kael');
placeUnit(playerSword1, 13, 13);
playerUnits.push(playerSword1);
var playerSword2 = new Unit('hero', 'player', 'sword', 2, 'Hikari');
placeUnit(playerSword2, 9, 14);
playerUnits.push(playerSword2);
var playerArmored = new Unit('armored', 'player', 'armored', 1, 'Brutus');
placeUnit(playerArmored, 14, 13);
playerUnits.push(playerArmored);
var playerMagic = new Unit('hero', 'player', 'magic', 1, 'Mizuki');
placeUnit(playerMagic, 9, 13);
playerUnits.push(playerMagic);
var playerDragonRider = new Unit('dragonRider', 'player', 'dragonRider', 1, 'Lyra');
placeUnit(playerDragonRider, 8, 12);
playerUnits.push(playerDragonRider);
var playerRider = new Unit('rider', 'player', 'rider', 1, 'Darius');
placeUnit(playerRider, 14, 12);
playerUnits.push(playerRider);
var playerHealer = new Unit('healer', 'player', 'healer', 1, 'Eirynn');
placeUnit(playerHealer, 10, 14);
playerUnits.push(playerHealer);
var playerBow = new Unit('hero', 'player', 'bow', 1, 'Elian');
placeUnit(playerBow, 11, 13);
playerUnits.push(playerBow);
var playerManakete = new Unit('manakete', 'player', 'manakete', 1, 'Irios');
placeUnit(playerManakete, 11, 12);
playerUnits.push(playerManakete);
// Enemy units (top-left corner)
var enemyCommander = new Unit('commander', 'enemy', 'sword', 1, 'Ardan');
placeUnit(enemyCommander, 2, 0);
enemyUnits.push(enemyCommander);
var enemySword1 = new Unit('hero', 'enemy', 'sword', 1, 'Kael');
placeUnit(enemySword1, 1, 1);
enemyUnits.push(enemySword1);
var enemySword2 = new Unit('hero', 'enemy', 'sword', 2, 'Hikari');
placeUnit(enemySword2, 5, 0);
enemyUnits.push(enemySword2);
var enemyArmored = new Unit('armored', 'enemy', 'armored', 1, 'Brutus');
placeUnit(enemyArmored, 0, 1);
enemyUnits.push(enemyArmored);
var enemyMagic = new Unit('hero', 'enemy', 'magic', 1, 'Mizuki');
placeUnit(enemyMagic, 5, 1);
enemyUnits.push(enemyMagic);
var enemyDragonRider = new Unit('dragonRider', 'enemy', 'dragonRider', 1, 'Lyra');
placeUnit(enemyDragonRider, 6, 2);
enemyUnits.push(enemyDragonRider);
var enemyRider = new Unit('rider', 'enemy', 'rider', 1, 'Darius');
placeUnit(enemyRider, 0, 2);
enemyUnits.push(enemyRider);
var enemyHealer = new Unit('healer', 'enemy', 'healer', 1, 'Eirynn');
placeUnit(enemyHealer, 4, 0);
enemyUnits.push(enemyHealer);
var enemyBow = new Unit('hero', 'enemy', 'bow', 1, 'Elian');
placeUnit(enemyBow, 3, 1);
enemyUnits.push(enemyBow);
var enemyManakete = new Unit('manakete', 'enemy', 'manakete', 1, 'Irios');
placeUnit(enemyManakete, 3, 2);
enemyUnits.push(enemyManakete);
}
function generateTerrain() {
// First, add normal terrain to all tiles
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x].addTerrain('normal');
}
}
// Add water river in middle of map (rows 6, 7, 8 - 3 tiles wide)
for (var y = 6; y <= 8; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x].addTerrain('water');
}
}
// Replace water tiles with bridge tiles at specified positions
// Bridge 1: columns 1-2, rows 6-8 (2 horizontal x 3 vertical)
for (var y = 6; y <= 8; y++) {
for (var x = 1; x <= 2; x++) {
grid[y][x].addTerrain('bridge');
}
}
// Bridge 2: columns 12-13, rows 6-8 (2 horizontal x 3 vertical)
for (var y = 6; y <= 8; y++) {
for (var x = 12; x <= 13; x++) {
grid[y][x].addTerrain('bridge');
}
}
// Add some trees scattered around (avoiding bridge areas) - trees go on top of normal terrain
var treePositions = [{
x: 4,
y: 4
}, {
x: 6,
y: 5
}, {
x: 9,
y: 4
}, {
x: 11,
y: 5
}, {
x: 3,
y: 10
}, {
x: 5,
y: 11
}, {
x: 8,
y: 10
}, {
x: 10,
y: 11
}, {
x: 7,
y: 3
}, {
x: 14,
y: 10
}, {
x: 0,
y: 12
}, {
x: 14,
y: 4
}];
// Add trees after all base terrain is placed to ensure proper layering
for (var i = 0; i < treePositions.length; i++) {
var pos = treePositions[i];
// First ensure there's normal terrain, then add tree on top
if (grid[pos.y][pos.x].terrainType !== 'normal') {
grid[pos.y][pos.x].addTerrain('normal');
}
// Add tree terrain graphics on top of normal terrain
var treeGraphics = grid[pos.y][pos.x].attachAsset('tree', {
anchorX: 0.5,
anchorY: 0.5
});
treeGraphics.alpha = 1.0;
// Set terrain type but keep the normal terrain graphics underneath
grid[pos.y][pos.x].terrainType = 'tree';
}
}
function placeUnit(unit, x, y) {
unit.gridX = x;
unit.gridY = y;
unit.x = grid[y][x].x;
unit.y = grid[y][x].y;
grid[y][x].unit = unit;
game.addChild(unit);
}
function clearHighlights() {
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x].hideHighlights();
}
}
}
function showMovementOptions(unit) {
// Use pathfinding to check reachable tiles considering terrain
var reachableTiles = getReachableTiles(unit);
for (var i = 0; i < reachableTiles.length; i++) {
var tile = reachableTiles[i];
grid[tile.y][tile.x].showHighlight();
}
}
function showAttackOptions(unit) {
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (unit.canAttack(x, y) && grid[y][x].unit && grid[y][x].unit.team !== unit.team && grid[y][x].unit.hp > 0) {
grid[y][x].showAttackRange();
}
}
}
}
function showHealOptions(unit) {
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (unit.canHeal(x, y) && grid[y][x].unit && grid[y][x].unit.team === unit.team && grid[y][x].unit.hp < grid[y][x].unit.maxHp) {
grid[y][x].showAttackRange();
}
}
}
}
function showAttackRangePreview(unit) {
var reachableTiles = getReachableTiles(unit);
var attackRange = unit.getRange();
var reachablePositions = {};
var attackOutlineTiles = {};
// Mark all reachable positions (blue movement range)
for (var i = 0; i < reachableTiles.length; i++) {
var tile = reachableTiles[i];
reachablePositions[tile.x + ',' + tile.y] = true;
// Show blue movement range
grid[tile.y][tile.x].showHighlight();
}
// Mark current position as reachable too
reachablePositions[unit.gridX + ',' + unit.gridY] = true;
// Calculate attack outline around the perimeter of movement range
var allMovementPositions = reachableTiles.slice();
allMovementPositions.push({
x: unit.gridX,
y: unit.gridY
});
// For each movement position, check outward for attack range
for (var i = 0; i < allMovementPositions.length; i++) {
var pos = allMovementPositions[i];
// Check all possible attack positions from this movement position
for (var dy = -attackRange; dy <= attackRange; dy++) {
for (var dx = -attackRange; dx <= attackRange; dx++) {
var distance = Math.abs(dx) + Math.abs(dy);
// Check if this distance is valid for the unit's attack pattern
var isValidAttackDistance = false;
if (unit.weaponType === 'bow') {
isValidAttackDistance = distance === 2; // Bow attacks only at exactly range 2, not range 1
} else if (unit.weaponType === 'manakete') {
isValidAttackDistance = distance >= 1 && distance <= 2; // Manakete attacks at range 1-2 like magic
} else if (unit.weaponType === 'healer') {
isValidAttackDistance = distance === 1; // Healer heals at range 1
} else {
isValidAttackDistance = distance >= 1 && distance <= attackRange; // Regular units attack from 1 to max range
}
if (isValidAttackDistance) {
var ax = pos.x + dx;
var ay = pos.y + dy;
if (ax >= 0 && ax < GRID_SIZE && ay >= 0 && ay < GRID_SIZE) {
var key = ax + ',' + ay;
// Only show attack outline if it's NOT a reachable movement position
if (!reachablePositions[key]) {
attackOutlineTiles[key] = {
x: ax,
y: ay,
isHeal: unit.weaponType === 'healer'
};
}
}
}
}
}
}
// Show attack outline tiles
for (var key in attackOutlineTiles) {
var pos = attackOutlineTiles[key];
if (pos.isHeal) {
grid[pos.y][pos.x].showHealRangePreview();
} else {
grid[pos.y][pos.x].showAttackRangePreview();
}
}
}
function getReachableTiles(unit) {
if (!unit) {
return [];
}
var reachableTiles = [];
var maxRange = unit.getMovementRange();
var visited = {};
var queue = [{
x: unit.gridX,
y: unit.gridY,
cost: 0
}];
visited[unit.gridX + ',' + unit.gridY] = true;
while (queue.length > 0) {
var current = queue.shift();
// Check all 4 directions
var directions = [{
x: 0,
y: 1
}, {
x: 0,
y: -1
}, {
x: 1,
y: 0
}, {
x: -1,
y: 0
}];
for (var d = 0; d < directions.length; d++) {
var dir = directions[d];
var newX = current.x + dir.x;
var newY = current.y + dir.y;
var key = newX + ',' + newY;
if (newX < 0 || newX >= GRID_SIZE || newY < 0 || newY >= GRID_SIZE) {
continue;
}
if (visited[key]) {
continue;
}
// Allow passing through same-team units during pathfinding, but not ending on them
var tileUnit = grid[newY][newX].unit;
if (tileUnit && tileUnit.team === unit.team) {
// Can pass through same-team units but cannot end movement here
// Continue pathfinding but mark this as a pass-through tile
} else if (tileUnit && tileUnit.team !== unit.team) {
// Cannot pass through enemy units
continue;
}
var moveCost = getMoveCost(unit, newX, newY);
if (moveCost === -1) {
continue;
} // Cannot move to this tile
var newCost = current.cost + moveCost;
if (newCost <= maxRange) {
visited[key] = true;
queue.push({
x: newX,
y: newY,
cost: newCost
});
if (newX !== unit.gridX || newY !== unit.gridY) {
reachableTiles.push({
x: newX,
y: newY,
cost: newCost
});
}
}
}
}
return reachableTiles;
}
function getMoveCost(unit, x, y) {
var tile = grid[y][x];
// Water - dragon riders and manaketes can cross
if (tile.terrainType === 'water' && unit.weaponType !== 'dragonRider' && unit.weaponType !== 'manakete') {
return -1; // Cannot move
}
// Tree - only dragon riders and manaketes can cross, all others cannot
if (tile.terrainType === 'tree') {
if (unit.weaponType === 'dragonRider' || unit.weaponType === 'manakete') {
return 1; // Normal cost for dragonRider and manakete
}
return -1; // Cannot move through trees for all other units
}
// Normal terrain and bridges
return 1;
}
function showEnemyMovementOptions(unit) {
var reachableTiles = getReachableTiles(unit);
for (var i = 0; i < reachableTiles.length; i++) {
var tile = reachableTiles[i];
grid[tile.y][tile.x].showHighlight();
}
}
function moveUnit(unit, targetX, targetY) {
// Check if target tile is occupied by another unit
if (grid[targetY][targetX].unit) {
// Find nearest valid empty tile
var validTile = findNearestValidTile(unit, targetX, targetY);
targetX = validTile.x;
targetY = validTile.y;
}
grid[unit.gridY][unit.gridX].unit = null;
unit.gridX = targetX;
unit.gridY = targetY;
grid[targetY][targetX].unit = unit;
tween(unit, {
x: grid[targetY][targetX].x,
y: grid[targetY][targetX].y
}, {
duration: 300
});
clearHighlights();
}
function isValidMove(unit, x, y) {
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
return false;
}
// Cannot end movement on a tile with any unit (same team or enemy)
if (grid[y][x].unit) {
return false;
}
var distance = Math.abs(unit.gridX - x) + Math.abs(unit.gridY - y);
if (distance > unit.getMovementRange()) {
return false;
}
// Check terrain restrictions
var targetTile = grid[y][x];
if (targetTile.terrainType === 'water') {
// Only dragon riders and manaketes can cross water
if (unit.weaponType !== 'dragonRider' && unit.weaponType !== 'manakete') {
return false;
}
}
return true;
}
function getTileFromPosition(screenX, screenY) {
var startX = (2048 - GRID_SIZE * TILE_SIZE) / 2;
var startY = (2732 - GRID_SIZE * TILE_SIZE) / 2;
var gridX = Math.floor((screenX - startX) / TILE_SIZE);
var gridY = Math.floor((screenY - startY) / TILE_SIZE);
if (gridX >= 0 && gridX < GRID_SIZE && gridY >= 0 && gridY < GRID_SIZE) {
return {
x: gridX,
y: gridY
};
}
return null;
}
// Enemy AI functions
function getValidMovesForUnit(unit) {
return getReachableTiles(unit);
}
function findNearestValidTile(unit, targetX, targetY) {
// If target tile is valid and empty, use it
if (targetX >= 0 && targetX < GRID_SIZE && targetY >= 0 && targetY < GRID_SIZE && !grid[targetY][targetX].unit && getMoveCost(unit, targetX, targetY) !== -1) {
return {
x: targetX,
y: targetY
};
}
// Search in expanding circles for nearest valid empty tile
for (var radius = 1; radius <= GRID_SIZE; radius++) {
for (var dy = -radius; dy <= radius; dy++) {
for (var dx = -radius; dx <= radius; dx++) {
// Only check tiles at current radius (perimeter of circle)
if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue;
var newX = targetX + dx;
var newY = targetY + dy;
if (newX >= 0 && newX < GRID_SIZE && newY >= 0 && newY < GRID_SIZE && !grid[newY][newX].unit && getMoveCost(unit, newX, newY) !== -1) {
return {
x: newX,
y: newY
};
}
}
}
}
// If no valid tile found, return current position
return {
x: unit.gridX,
y: unit.gridY
};
}
function updateUnitCounters() {
var alivePlayerUnits = 0;
var aliveEnemyUnits = 0;
// Count alive player units
for (var i = 0; i < playerUnits.length; i++) {
if (playerUnits[i] && playerUnits[i].hp > 0) {
alivePlayerUnits++;
}
}
// Count alive enemy units
for (var i = 0; i < enemyUnits.length; i++) {
if (enemyUnits[i] && enemyUnits[i].hp > 0) {
aliveEnemyUnits++;
}
}
// Update counter display
playerCounter.setText(alivePlayerUnits + '/10');
enemyCounter.setText(aliveEnemyUnits + '/10');
}
function findAttackTargets(unit) {
var targets = [];
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (unit.canAttack(x, y) && grid[y][x].unit && grid[y][x].unit.team !== unit.team && grid[y][x].unit.hp > 0) {
targets.push({
unit: grid[y][x].unit,
x: x,
y: y
});
}
}
}
return targets;
}
function checkAutoEndTurn() {
if (currentTurn !== 'player') {
return;
}
var allMoved = true;
for (var i = 0; i < playerUnits.length; i++) {
if (!playerUnits[i].hasMoved && playerUnits[i].hp > 0) {
allMoved = false;
break;
}
}
if (allMoved) {
// Auto end turn
LK.setTimeout(function () {
for (var i = 0; i < playerUnits.length; i++) {
playerUnits[i].resetTurn();
}
currentTurn = 'enemy';
turnText.setText('Enemy Turn');
phaseText.setText('Enemy AI thinking...');
LK.setTimeout(executeEnemyAI, 1000);
}, 500);
}
}
function executeEnemyAI() {
if (currentTurn !== 'enemy') {
return;
}
// Hide end turn button during enemy turn
endTurnBtn.alpha = 0;
endTurnBtn.interactive = false;
var currentEnemyIndex = 0;
function processNextEnemy() {
if (currentEnemyIndex >= enemyUnits.length) {
// All enemies processed, end turn
for (var i = 0; i < enemyUnits.length; i++) {
enemyUnits[i].resetTurn();
}
currentTurn = 'player';
turnText.setText('Player Turn');
selectedUnit = null;
selectedEnemyUnit = null;
turnPhase = 'select';
phaseText.setText('Select Unit');
clearHighlights();
removeActionButtons();
// Show end turn button again for player turn
endTurnBtn.alpha = 1;
endTurnBtn.interactive = true;
return;
}
var enemy = enemyUnits[currentEnemyIndex];
currentEnemyIndex++;
if (enemy.hasMoved || enemy.hp <= 0) {
processNextEnemy();
return;
}
// Check if enemy can attack from current position
var currentTargets = findAttackTargets(enemy);
if (currentTargets.length > 0) {
// Attack the first available target
var target = currentTargets[0].unit;
// Add attack shake animation
var originalX = enemy.x;
tween(enemy, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX
}, {
duration: 100
});
}
});
}
});
LK.getSound('Impacts').play();
showAttackEffect(target);
var isDead = target.takeDamage(enemy.attack, enemy.weaponType);
// Check for counterattack if target survived
if (!isDead) {
var attackerDied = target.counterAttack(enemy);
if (attackerDied) {
grid[enemy.gridY][enemy.gridX].unit = null;
LK.getSound('Vanish').play();
tween(enemy, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
enemy.destroy();
}
});
var index = enemyUnits.indexOf(enemy);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
if (enemy.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
return;
}
}
if (isDead) {
grid[target.gridY][target.gridX].unit = null;
LK.getSound('Vanish').play();
tween(target, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
target.destroy();
}
});
if (target.team === 'player') {
var index = playerUnits.indexOf(target);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
// Check if player commander was defeated
if (target.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
}
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
return;
}
// Try to move closer to player units
var validMoves = getValidMovesForUnit(enemy);
if (validMoves.length > 0) {
// Find the move that gets closest to any player unit
var bestMove = null;
var bestScore = Infinity;
for (var m = 0; m < validMoves.length; m++) {
var move = validMoves[m];
var minDistanceToPlayer = Infinity;
for (var p = 0; p < playerUnits.length; p++) {
if (playerUnits[p].hp <= 0) {
continue;
}
var dist = Math.abs(move.x - playerUnits[p].gridX) + Math.abs(move.y - playerUnits[p].gridY);
minDistanceToPlayer = Math.min(minDistanceToPlayer, dist);
}
if (minDistanceToPlayer < bestScore) {
bestScore = minDistanceToPlayer;
bestMove = move;
}
}
if (bestMove) {
// Find nearest valid tile if bestMove position is occupied
var finalMove = findNearestValidTile(enemy, bestMove.x, bestMove.y);
moveUnit(enemy, finalMove.x, finalMove.y);
LK.setTimeout(function () {
// Check if can attack after moving
var targetsAfterMove = findAttackTargets(enemy);
if (targetsAfterMove.length > 0) {
var target = targetsAfterMove[0].unit;
// Add attack shake animation
var originalX = enemy.x;
tween(enemy, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
tween(enemy, {
x: originalX
}, {
duration: 100
});
}
});
}
});
LK.getSound('Impacts').play();
showAttackEffect(target);
var isDead = target.takeDamage(enemy.attack, enemy.weaponType);
// Check for counterattack if target survived
if (!isDead) {
var attackerDied = target.counterAttack(enemy);
if (attackerDied) {
grid[enemy.gridY][enemy.gridX].unit = null;
LK.getSound('Vanish').play();
tween(enemy, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
enemy.destroy();
}
});
var index = enemyUnits.indexOf(enemy);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
if (enemy.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
return;
}
}
if (isDead) {
grid[target.gridY][target.gridX].unit = null;
LK.getSound('Vanish').play();
tween(target, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
target.destroy();
}
});
if (target.team === 'player') {
var index = playerUnits.indexOf(target);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
// Check if player commander was defeated
if (target.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
}
}
}
enemy.endTurn();
LK.setTimeout(processNextEnemy, 500);
}, 400);
return;
}
}
// No valid moves, end this unit's turn
enemy.endTurn();
LK.setTimeout(processNextEnemy, 300);
}
processNextEnemy();
}
// End turn button
endTurnBtn.interactive = true;
endTurnBtn.down = function () {
if (gameState !== 'playing') {
return;
}
if (currentTurn === 'player') {
for (var i = 0; i < playerUnits.length; i++) {
playerUnits[i].resetTurn();
}
currentTurn = 'enemy';
turnText.setText('Enemy Turn');
phaseText.setText('Enemy AI thinking...');
LK.setTimeout(executeEnemyAI, 1000);
} else {
for (var i = 0; i < enemyUnits.length; i++) {
enemyUnits[i].resetTurn();
}
currentTurn = 'player';
turnText.setText('Player Turn');
}
selectedUnit = null;
selectedEnemyUnit = null;
originalUnitPosition = null;
turnPhase = 'select';
phaseText.setText('Select Unit');
clearHighlights();
removeActionButtons();
};
// Logo and follow text variables
var logoSprite = null;
var followText = null;
// Create and show logo and follow text on menu
logoSprite = LK.getAsset('Logo', {
anchorX: 0,
anchorY: 0,
scaleX: 1.2,
scaleY: 1.2
});
logoSprite.x = 120;
logoSprite.y = 20;
LK.gui.topLeft.addChild(logoSprite);
followText = new Text2('FOLLOW ME: @Mugiwara_Shanks', {
size: 28,
fill: 0xFFFFFF
});
followText.anchor.set(0, 0);
followText.x = 250;
followText.y = 50;
LK.gui.topLeft.addChild(followText);
// Play main menu music when the game starts
LK.playMusic('MainMenu');
// Menu click handler - start game when clicking anywhere on menu
game.down = function (x, y, obj) {
// Only handle menu clicks when in menu state
if (gameState === 'menu') {
// Stop main menu music when starting the game
LK.stopMusic();
startGame();
return;
}
// Only handle game clicks when in playing state
if (gameState !== 'playing') {
return;
}
var tilePos = getTileFromPosition(x, y);
if (!tilePos) {
return;
}
var clickedTile = grid[tilePos.y][tilePos.x];
if (turnPhase === 'select') {
// Handle enemy unit selection for viewing movement
if (clickedTile.unit && clickedTile.unit.team !== currentTurn) {
if (selectedEnemyUnit === clickedTile.unit) {
// Clicking same enemy again - hide movement
selectedEnemyUnit = null;
clearHighlights();
hideUnitInfo();
} else {
// Clicking different enemy - show movement and attack range
selectedEnemyUnit = clickedTile.unit;
clearHighlights();
showEnemyMovementOptions(selectedEnemyUnit);
showAttackRangePreview(selectedEnemyUnit);
showUnitInfo(selectedEnemyUnit);
}
return;
}
// Clear enemy selection when clicking elsewhere
if (selectedEnemyUnit) {
selectedEnemyUnit = null;
clearHighlights();
}
// Handle player unit selection - allow viewing stats of moved units
if (clickedTile.unit && clickedTile.unit.team === currentTurn) {
// If unit has already moved, just show info and return
if (clickedTile.unit.hasMoved) {
showUnitInfo(clickedTile.unit);
return;
}
selectedUnit = clickedTile.unit;
originalUnitPosition = {
x: selectedUnit.gridX,
y: selectedUnit.gridY
};
// Show unit info
showUnitInfo(selectedUnit);
// Check for attack targets from current position on first click
var hasAttackTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canAttack(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team !== selectedUnit.team && grid[ty][tx].unit.hp > 0) {
hasAttackTargets = true;
break;
}
}
if (hasAttackTargets) {
break;
}
}
if (hasAttackTargets) {
clearHighlights();
// Create move button
var moveBtn = new Text2('Move', {
size: 40,
fill: 0x00FF00
});
moveBtn.anchor.set(0.5, 1);
moveBtn.y = -120;
LK.gui.bottom.addChild(moveBtn);
moveBtn.interactive = true;
moveBtn.down = function () {
// Remove action buttons and continue with move
LK.gui.bottom.removeChild(moveBtn);
if (attackBtn) {
LK.gui.bottom.removeChild(attackBtn);
attackBtn = null;
}
if (ignoreBtn) {
LK.gui.bottom.removeChild(ignoreBtn);
ignoreBtn = null;
}
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
};
// Show attack and ignore buttons
turnPhase = 'chooseAction';
phaseText.setText('Attack, Ignore or Move');
showActionButtons();
} else {
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
showAttackRangePreview(selectedUnit);
}
}
} else if (turnPhase === 'move') {
// Cancel move selection by clicking anywhere
if (!isValidMove(selectedUnit, tilePos.x, tilePos.y)) {
selectedUnit = null;
originalUnitPosition = null;
turnPhase = 'select';
phaseText.setText('Select Unit');
clearHighlights();
hideUnitInfo();
return;
}
if (isValidMove(selectedUnit, tilePos.x, tilePos.y)) {
moveUnit(selectedUnit, tilePos.x, tilePos.y);
LK.setTimeout(function () {
if (!selectedUnit) {
return;
}
if (selectedUnit.weaponType === 'healer') {
var hasHealTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canHeal(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team === selectedUnit.team && grid[ty][tx].unit.hp < grid[ty][tx].unit.maxHp) {
hasHealTargets = true;
break;
}
}
if (hasHealTargets) {
break;
}
}
if (hasHealTargets) {
turnPhase = 'chooseAction';
phaseText.setText('Heal or Ignore');
showActionButtons();
} else {
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
}
} else {
var hasAttackTargets = false;
for (var ty = 0; ty < GRID_SIZE; ty++) {
for (var tx = 0; tx < GRID_SIZE; tx++) {
if (selectedUnit.canAttack(tx, ty) && grid[ty][tx].unit && grid[ty][tx].unit.team !== selectedUnit.team && grid[ty][tx].unit.hp > 0) {
hasAttackTargets = true;
break;
}
}
if (hasAttackTargets) {
break;
}
}
if (hasAttackTargets) {
turnPhase = 'chooseAction';
phaseText.setText('Attack or Ignore');
showActionButtons();
} else {
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
}
}
}, 350);
}
} else if (turnPhase === 'heal') {
if (clickedTile.unit && clickedTile.unit.team === selectedUnit.team && selectedUnit.canHeal(tilePos.x, tilePos.y)) {
var target = clickedTile.unit;
selectedUnit.heal(target);
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
} else {
// Clicked elsewhere during heal - return to original position
if (originalUnitPosition && selectedUnit) {
moveUnit(selectedUnit, originalUnitPosition.x, originalUnitPosition.y);
LK.setTimeout(function () {
if (selectedUnit) {
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
}
}, 350);
}
}
} else if (turnPhase === 'attack') {
if (clickedTile.unit && clickedTile.unit.team !== selectedUnit.team && selectedUnit.canAttack(tilePos.x, tilePos.y)) {
var target = clickedTile.unit;
// Add attack shake animation
if (!selectedUnit) {
return;
}
var originalX = selectedUnit.x;
tween(selectedUnit, {
x: originalX + 10
}, {
duration: 100,
onFinish: function onFinish() {
if (!selectedUnit) {
return;
}
tween(selectedUnit, {
x: originalX - 10
}, {
duration: 100,
onFinish: function onFinish() {
if (!selectedUnit) {
return;
}
tween(selectedUnit, {
x: originalX
}, {
duration: 100
});
}
});
}
});
LK.getSound('Impacts').play();
showAttackEffect(target);
var isDead = target.takeDamage(selectedUnit.attack, selectedUnit.weaponType);
// Check for counterattack if target survived
if (!isDead) {
var attackerDied = target.counterAttack(selectedUnit);
if (attackerDied) {
grid[selectedUnit.gridY][selectedUnit.gridX].unit = null;
LK.getSound('Vanish').play();
var dyingUnit = selectedUnit; // Store reference before selectedUnit becomes null
tween(selectedUnit, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (dyingUnit) {
dyingUnit.destroy();
}
}
});
if (selectedUnit.team === 'player') {
var index = playerUnits.indexOf(selectedUnit);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
if (selectedUnit.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
} else {
var index = enemyUnits.indexOf(selectedUnit);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
if (selectedUnit.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
}
}
}
if (isDead) {
grid[target.gridY][target.gridX].unit = null;
LK.getSound('Vanish').play();
tween(target, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
target.destroy();
}
});
if (target.team === 'player') {
var index = playerUnits.indexOf(target);
if (index > -1) {
playerUnits.splice(index, 1);
}
updateUnitCounters();
// Check if player commander was defeated
if (target.type === 'commander') {
LK.playMusic('YouLose');
LK.showGameOver();
return;
}
} else {
var index = enemyUnits.indexOf(target);
if (index > -1) {
enemyUnits.splice(index, 1);
}
updateUnitCounters();
// Check if enemy commander was defeated
if (target.type === 'commander') {
LK.playMusic('Victory');
LK.showYouWin();
return;
}
}
}
selectedUnit.endTurn();
turnPhase = 'select';
phaseText.setText('Select Unit');
selectedUnit = null;
originalUnitPosition = null;
clearHighlights();
checkAutoEndTurn();
} else {
// Clicked elsewhere during attack - return to original position
if (originalUnitPosition && selectedUnit) {
moveUnit(selectedUnit, originalUnitPosition.x, originalUnitPosition.y);
LK.setTimeout(function () {
if (selectedUnit) {
turnPhase = 'move';
phaseText.setText('Choose destination');
showMovementOptions(selectedUnit);
}
}, 350);
}
}
}
};
function showAttackEffect(target) {
var attackEffect = LK.getAsset('attackEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
attackEffect.x = target.x;
attackEffect.y = target.y;
game.addChild(attackEffect);
// Flash effect - fade in quickly then fade out
tween(attackEffect, {
alpha: 0.8,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 150,
onFinish: function onFinish() {
tween(attackEffect, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
onFinish: function onFinish() {
game.removeChild(attackEffect);
}
});
}
});
}
function showHealEffect(target) {
var healEffect = LK.getAsset('healEffect', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
healEffect.x = target.x;
healEffect.y = target.y;
healEffect.tint = 0x00FF00; // Green tint for healing
game.addChild(healEffect);
// Flash effect - fade in quickly then fade out
tween(healEffect, {
alpha: 0.8,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 150,
onFinish: function onFinish() {
tween(healEffect, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
onFinish: function onFinish() {
game.removeChild(healEffect);
}
});
}
});
}
function startGame() {
// Hide logo and follow text when starting game
if (logoSprite) {
LK.gui.topLeft.removeChild(logoSprite);
logoSprite = null;
}
if (followText) {
LK.gui.topLeft.removeChild(followText);
followText = null;
}
// Show title and objective message first
var titleText = new Text2('ICE EMBLEM', {
size: 120,
fill: 0x00BFFF
});
titleText.anchor.set(0.5, 1);
titleText.y = -200;
LK.gui.bottom.addChild(titleText);
var objectiveText = new Text2('DEFEAT THE COMMANDER', {
size: 100,
fill: 0xFFD700
});
objectiveText.anchor.set(0.5, 1);
objectiveText.y = -100;
LK.gui.bottom.addChild(objectiveText);
// Fade in title and objective text together
tween(titleText, {
alpha: 1
}, {
duration: 1000
});
// Fade in and then fade out the objective text
tween(objectiveText, {
alpha: 1
}, {
duration: 1000,
onFinish: function onFinish() {
LK.setTimeout(function () {
// Fade out both title and objective text
tween(titleText, {
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
LK.gui.bottom.removeChild(titleText);
}
});
tween(objectiveText, {
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
LK.gui.bottom.removeChild(objectiveText);
}
});
}, 2000);
}
});
LK.setTimeout(function () {
// Show game UI after objective text fades out
turnText.alpha = 1;
phaseText.alpha = 1;
playerCounter.alpha = 1;
enemyCounter.alpha = 1;
endTurnBtn.alpha = 1;
endTurnBtn.interactive = true;
unitInfoPanel.visible = true;
// Change game state
gameState = 'playing';
// Initialize and start game
initializeGrid();
initializeUnits();
updateUnitCounters();
// Play background music
LK.playMusic('MedievalMusicFantasy1');
}, 4000);
}
Un arbusto frondoso estilo animado. In-Game asset. 2d. High contrast. No shadows
Un cuadrado perfecto, sin ningún contorno, gris y con textura de piedra. In-Game asset. 2d. High contrast. No shadows
Un joven comandante de porte noble, con cabello plateado profundo ligeramente desordenado y una diadema dorada. Sus ojos son grandes y dorados. Viste una armadura de placas negro adornada con bordes dorados, con hombreras firmes y un broche rojo brillante en el pecho. Lleva una capa roja que cae sobre su hombro izquierdo. Su expresión es seria y firme. Estilo chibi fullbody In-Game asset. 2d. High contrast. No shadows
Un guerrero acorazado en estilo chibi de cuerpo completo, de cuerpo bajo y ancho, con una gran armadura pesada de hierro gris oscuro que cubre casi todo su cuerpo. Un casco de hierro de aspecto rudo, dejando ver un bigote italiano chistoso y exagerado. La armadura es muy voluminosa, con hombreras redondeadas, brazales gruesos y botas metálicas, transmitiendo la idea de fortaleza e impenetrabilidad. Sostiene un enorme hacha de batalla de doble filo, desproporcionada en estilo chibi, que enfatiza su poder de ataque. Su expresión es ruda pero cómica gracias al bigote, dándole un aire intimidante y gracioso a la vez.. In-Game asset. 2d. High contrast. No shadows
Una sanadora en estilo chibi/cuerpo completo, una chica joven de físico algo marcado por el esfuerzo y la vida dura. Viste ropas tribales, va descalza. Su cabello es negro, largo y despeinado, cayendo con un aire descuidado pero natural. Porta un bastón improvisado hecho de una rama de árbol torcida, aún con algunas hojas pegadas, lo que resalta su carácter rústico y tribal. En su piel se notan tatuajes o marcas tribales, además de collares o pulseras hechos de hueso, madera o cuentas simples, que refuerzan su conexión espiritual con la naturaleza. Su expresión es serena y determinada, mostrando que a pesar de su apariencia austera y primitiva, es una guía y protectora para su gente.. In-Game asset. 2d. High contrast. No shadows
Una jinete de wyvern en estilo chibi/cuerpo completo. El wyvern es grande y de color morado, con alas extendidas y expresión fiera. La jinete es una joven guerrera con armadura ligera de torso en color verde con detalles aqua, con hombreras y guantes. Lleva pantalones blancos y botas verdes altas. Su cabello es corto estilo bob, de color aqua, y tiene ojos azules brillantes. Montada sobre el wyvern, sostiene con una mano las riendas y con la otra una lanza larga.. In-Game asset. 2d. High contrast. No shadows
Un manakete en estilo chibi/cuerpo completo. Se ve como un niño pequeño con cabello blanco desordenado. Sus alas de dragón, morado oscura y membranosas, están enrolladas alrededor de su cuerpo como si fueran una capucha, dándole un aire misterioso. Viste ropa casual y simple, algo holgada, que oculta sus verdaderos atributos. En sus brazos lleva vendas mal puestas, apenas cubriendo las escamas moradas de dragón que intentan asomarse bajo ellas, garras en las manos. En cada mejilla tiene tres marcas visibles como arañazos, y un colmillo visible. Su expresión es inocente pero enigmática, transmitiendo que no es un simple niño. El estilo es chibi pero con un aire ligeramente oscuro y mágico.. In-Game asset. 2d. High contrast. No shadows
Un caballero jinete en estilo chibi/cuerpo completo. Es un hombre delgado de aproximadamente 50 años, con barba de chivo bien marcada y expresión seria pero orgullosa. Lleva una armadura roja con detalles plateados brillantes, elegante y clásica. Porta una larga lanza de justas en una mano y un gran escudo ovalado en la otra, el cual está muy decorado con símbolos y adornos llamativos. Está montado sobre un caballo robusto y elegante, en pose firme y heroica. El estilo es chibi, pero mantiene la imponencia de un caballero veterano.. In-Game asset. 2d. High contrast. No shadows
Un arquero en estilo chibi/cuerpo completo. Es un joven de cabello castaño y tez blanca, con un aire humilde y campesino. Lleva ropa sencilla de campesino, con un parche en el ojo derecho que le da un toque rudo. Porta un arco de color dorado brillante y un carcaj lleno de flechas que lleva a modo de cinturón en la cintura. En la cabeza luce un sombrero vueltiao típico, ancho y llamativo. Está en pose de combate ligera, como apuntando o sosteniendo el arco listo para disparar, manteniendo el estilo simpático y expresivo de un chibi.. In-Game asset. 2d. High contrast. No shadows
Una maga en estilo chibi/cuerpo completo. Es una joven de unos 20 años, de cabello negro largo y suelto, con piel blanca pálida y ojeras marcadas que le dan un aire misterioso. Viste una capa morada que cubre su espalda y cae con elegancia, con guantes a juego del mismo color. Su atuendo consiste en una camiseta blanca sin mangas, pantalón negro ajustado y protecciones en los codos y rodillas para un toque práctico. En el cuello lleva un collar con un amuleto en forma de ojo dorado brillante, que resalta como símbolo mágico. Su expresión es seria y enigmática, transmitiendo el aire de una hechicera experimentada a pesar de su juventud.. In-Game asset. 2d. High contrast. No shadows
Un espadachín en estilo chibi/cuerpo completo. Es un joven de cabello negro algo desordenado, piel clara y expresión confiada, con una sonrisa desafiante. Viste un abrigo largo rojo hasta los tobillos, abierto al frente, con bordes dorados y un cinturón que ajusta a la cintura. Bajo el abrigo lleva una camiseta oscura, pantalones grises ajustados y botas rojas con detalles dorados. En su cintura lleva correas y fundas decorativas. Porta una gran espada de hoja plateada con brillo metálico y detalles en rojo, con la empuñadura dorada. Su pose transmite seguridad y carisma, como si estuviera listo para un combate pero también disfrutara de la situación.. In-Game asset. 2d. High contrast. No shadows
Una espadachina en estilo chibi/cuerpo completo. Es una mujer samurái de unos 30 años con cabello largo y marrón, atado ligeramente hacia atrás para que no estorbe en combate. Lleva una banda negra que cubre sus ojos. Su vestimenta es una túnica japonesa de batalla color azul cielo, de corte práctico y elegante, con detalles sutiles que insinúan resistencia. Viste unas botas japonesas de doble zanco (geta alta) que le dan un aire tradicional. Porta una katana brillante de hoja plateada y empuñadura simple, sostenida con firmeza en una mano, mientras la otra reposa relajada. Su expresión es calmada y disciplinada, transmitiendo seguridad y experiencia en combate.. In-Game asset. 2d. High contrast. No shadows
Efecto de Impacto sin letras. In-Game asset. 2d. High contrast. No shadows
Un destello verde blancuzco. In-Game asset. 2d. High contrast. No shadows