User prompt
Please fix the bug: 'storage.remove is not a function' in or related to this line: 'storage.remove('selectedParty');' Line Number: 353 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'storage.get is not a function' in or related to this line: 'var savedParty = storage.get('selectedParty');' Line Number: 228 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Make the stats carry over to each dungeon
User prompt
make a goblin enemy with 10 HP giving 3EXP for tower one and the tower one boss a gargoyle with 50HP giving 50 EXP
Code edit (1 edits merged)
Please save this source code
User prompt
Tower Orbs: Heartbound Quest
Initial prompt
make a tiny RPG Game where you start in a house and get a white mage, thief, and martial artist. make 4 towers each one is a dungeon with a boss at the end of each tower giving you a orb, you need 4 orbs to get to the 5th tower for the final boss and make the battles like undertale, where you have to use a heart to dodge th enemy projectiles
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var AmberBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 70;
self.maxHP = 70;
self.exp = 70;
self.sprite = self.attachAsset('boss_amber', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Boss Classes
var AmethystBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 50;
self.maxHP = 50;
self.exp = 50;
self.sprite = self.attachAsset('boss_amethyst', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var BatEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_bat', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var CrimsonBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 80;
self.maxHP = 80;
self.exp = 80;
self.sprite = self.attachAsset('boss_crimson', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var EmeraldBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 60;
self.maxHP = 60;
self.exp = 60;
self.sprite = self.attachAsset('boss_emerald', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Enemy Bullet
var EnemyBullet = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('enemy_bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.vx = 0;
self.vy = 0;
self.radius = 30;
self.active = true;
self.hp = 1; // Default, will be set to 2 for boss fight bullets
self.init = function (x, y, vx, vy, isBossFight) {
self.x = x;
self.y = y;
self.vx = vx;
self.vy = vy;
self.hp = isBossFight ? 2 : 1;
};
self.update = function () {
if (!self.active) return;
self.x += self.vx;
self.y += self.vy;
// Offscreen check
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
self.active = false;
self.destroy();
}
};
return self;
});
// Minor Enemy Classes
var GoblinEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Heart (player dodge object)
var Heart = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 40;
self.invincible = false;
self.invincibleTicks = 0;
self.setInvincible = function (ticks) {
self.invincible = true;
self.invincibleTicks = ticks;
self.sprite.alpha = 0.5;
};
self.update = function () {
if (self.invincible) {
self.invincibleTicks--;
if (self.invincibleTicks <= 0) {
self.invincible = false;
self.sprite.alpha = 1;
}
}
};
return self;
});
// HeartBullet (player's yellow bullet)
var HeartBullet = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xb8b031,
// yellow
width: 60,
height: 60,
shape: 'box'
});
self.radius = 30;
self.vx = 0;
self.vy = -18;
self.active = true;
self.init = function (x, y) {
self.x = x;
self.y = y;
};
self.update = function () {
if (!self.active) return;
self.y += self.vy;
// Offscreen check
if (self.y < -100) {
self.active = false;
self.destroy();
}
};
return self;
});
var ImpEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_imp', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var MimicEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
// Randomly select a mimic sprite variant
var mimicVariants = ['enemy_mimic', 'enemy_mimic2', 'enemy_mimic3'];
var idx = Math.floor(Math.random() * mimicVariants.length);
self.sprite = self.attachAsset(mimicVariants[idx], {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var ObsidianBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 100;
self.maxHP = 100;
self.exp = 100;
self.sprite = self.attachAsset('boss_obsidian', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Orb Icon
var OrbIcon = Container.expand(function () {
var self = Container.call(this);
self.orbId = 1;
self.collected = false;
self.icon = null;
self.setOrb = function (orbId, collected) {
self.orbId = orbId;
self.collected = collected;
if (self.icon) self.icon.destroy();
var assetId = 'orb' + orbId;
self.icon = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.icon.alpha = collected ? 1 : 0.3;
};
return self;
});
// Party Member Icon
var PartyIcon = Container.expand(function () {
var self = Container.call(this);
self.type = null;
self.selected = false;
self.icon = null;
self.setType = function (type) {
self.type = type;
if (self.icon) self.icon.destroy();
var assetId = '';
if (type === 'white_mage') assetId = 'icon_white_mage';
if (type === 'thief') assetId = 'icon_thief';
if (type === 'martial') assetId = 'icon_martial';
if (type === 'knight') assetId = 'icon_knight';
self.icon = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.setSelected = function (selected) {
self.selected = selected;
if (self.icon) {
self.icon.alpha = selected ? 1 : 0.6;
self.icon.scaleX = self.icon.scaleY = selected ? 1.15 : 1;
}
};
return self;
});
var RatEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_rat', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var SlimeEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_slime', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var SpriteEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_sprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// SwordEvent: sword that appears randomly during boss fight
var SwordEvent = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('sword_event', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 60;
self.active = true;
self.vanishTicks = 120; // lasts for 2 seconds
self.update = function () {
if (!self.active) return;
self.vanishTicks--;
if (self.vanishTicks <= 0) {
self.active = false;
self.destroy();
}
};
return self;
});
// Tower Icon
var TowerIcon = Container.expand(function () {
var self = Container.call(this);
self.towerId = 1;
self.locked = false;
self.icon = null;
self.setTower = function (towerId, locked) {
self.towerId = towerId;
self.locked = locked;
if (self.icon) self.icon.destroy();
var assetId = 'tower' + towerId;
self.icon = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 1
});
// Tint red if EX mode is active
if (typeof game !== "undefined" && game._exModeActive) {
// Use tween for instant tint
tween(self.icon, {
tint: 0xFF3333
}, {
duration: 0
});
}
self.icon.alpha = locked ? 0.4 : 1;
};
return self;
});
var WispEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_wisp', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222233
});
/****
* Game Code
****/
// sword image for boss event
// --- PLAYER TURN MENU REMOVED ---
// (player turn menu and related variables removed)
var shopMenu = null; // Ensure shopMenu is defined globally
var shopActive = false; // Ensure shopActive is defined globally
// Enemy images
// --- STORAGE PLUGIN ---
// Music
// Sound effects
// Orb icons
// Tower icons
// Enemy bullet
// Heart (player dodge object)
// Party member icons
// Game state
var STATE_PARTY_SELECT = 0;
var STATE_TOWER_SELECT = 1;
var STATE_BATTLE = 2;
var STATE_BOSS_DEFEAT = 3;
var STATE_GAME_COMPLETE = 4;
var gameState = STATE_PARTY_SELECT;
// Party selection
var partyMembers = ['white_mage', 'thief', 'martial', 'knight'];
// Load persistent party and orbs if available
var selectedParty = [];
var collectedOrbs = [false, false, false, false];
(function () {
var savedParty = storage.selectedParty;
var savedOrbs = storage.collectedOrbs;
if (savedParty && Array.isArray(savedParty) && savedParty.length === 3) {
selectedParty = savedParty.slice();
}
if (savedOrbs && Array.isArray(savedOrbs) && savedOrbs.length === 4) {
collectedOrbs = savedOrbs.slice();
}
})();
var partyIcons = [];
var partySelectText = null;
// Tower selection
var towers = [{
id: 1,
name: 'Amethyst Tower',
orb: 1
}, {
id: 2,
name: 'Emerald Tower',
orb: 2
}, {
id: 3,
name: 'Amber Tower',
orb: 3
}, {
id: 4,
name: 'Crimson Tower',
orb: 4
}, {
id: 5,
name: 'Obsidian Tower',
orb: 0
}];
var towerIcons = [];
var orbIcons = [];
var towerSelectText = null;
// Battle
var heart = null;
var heartStartX = 0;
var heartStartY = 0;
var heartDragging = false;
var heartDragOffsetX = 0;
var heartDragOffsetY = 0;
var battleBullets = [];
var battleTimer = 0;
var battlePhase = 0;
var battleBoss = null;
var battleBossId = 0;
var battleBossHP = 0;
var battleBossMaxHP = 0;
var battleBossText = null;
var battleHPText = null;
var battleOrbsText = null;
var battleActive = false;
var battleWin = false;
var battleLose = false;
var battleEndTimeout = null;
// GUI
var guiParty = [];
var guiOrbs = [];
var guiHP = null;
// --- CHARACTER STATS ---
var characterStats = {
white_mage: {
name: "White Mage",
hp: 18,
atk: 6,
def: 8,
desc: "Heals and supports the party."
},
thief: {
name: "Thief",
hp: 14,
atk: 10,
def: 5,
desc: "Fast and nimble, high critical chance."
},
martial: {
name: "Martial Artist",
hp: 20,
atk: 8,
def: 7,
desc: "Balanced fighter with strong attacks."
},
knight: {
name: "Knight",
hp: 24,
atk: 7,
def: 12,
desc: "High defense, protects the party."
}
};
// Track current HP for each party member during battle
var partyCurrentHP = [];
var statTextObjs = [];
var resetButton = null;
function showCharacterStatsAndReset() {
// Remove old stats if present
for (var i = 0; i < statTextObjs.length; i++) statTextObjs[i].destroy();
statTextObjs = [];
if (resetButton) {
resetButton.destroy();
resetButton = null;
}
var statsY = 2500;
var statsX = 200;
var spacing = 480;
for (var i = 0; i < partyMembers.length; i++) {
var type = partyMembers[i];
var stats = characterStats[type];
var txt = new Text2(stats.name + " HP:" + stats.hp + " ATK:" + stats.atk + " DEF:" + stats.def, {
size: 60,
fill: "#fff"
});
txt.anchor.set(0, 0);
txt.x = statsX + i * spacing;
txt.y = statsY;
LK.gui.bottom.addChild(txt);
statTextObjs.push(txt);
}
// Add reset button below stats
resetButton = new Text2("Reset Game Data", {
size: 70,
fill: 0xFF3366
});
resetButton.anchor.set(0.5, 0);
resetButton.x = 2048 / 2;
resetButton.y = statsY + 120;
resetButton.interactive = true;
resetButton.buttonMode = true;
resetButton._isResetButton = true;
LK.gui.bottom.addChild(resetButton);
}
function clearCharacterStatsAndReset() {
for (var i = 0; i < statTextObjs.length; i++) statTextObjs[i].destroy();
statTextObjs = [];
if (resetButton) {
resetButton.destroy();
resetButton = null;
}
}
// Utility
function resetGame() {
// Reset all state
gameState = STATE_PARTY_SELECT;
selectedParty = [];
collectedOrbs = [false, false, false, false];
for (var i = 0; i < partyIcons.length; i++) partyIcons[i].destroy();
partyIcons = [];
for (var i = 0; i < towerIcons.length; i++) towerIcons[i].destroy();
towerIcons = [];
for (var i = 0; i < orbIcons.length; i++) orbIcons[i].destroy();
orbIcons = [];
if (partySelectText) {
partySelectText.destroy();
partySelectText = null;
}
if (towerSelectText) {
towerSelectText.destroy();
towerSelectText = null;
}
if (heart) {
heart.destroy();
heart = null;
}
for (var i = 0; i < battleBullets.length; i++) battleBullets[i].destroy();
battleBullets = [];
if (battleBossText) {
battleBossText.destroy();
battleBossText = null;
}
if (battleHPText) {
battleHPText.destroy();
battleHPText = null;
}
if (battleOrbsText) {
battleOrbsText.destroy();
battleOrbsText = null;
}
if (battleEndTimeout) {
LK.clearTimeout(battleEndTimeout);
battleEndTimeout = null;
}
for (var i = 0; i < guiParty.length; i++) guiParty[i].destroy();
guiParty = [];
for (var i = 0; i < guiOrbs.length; i++) guiOrbs[i].destroy();
guiOrbs = [];
if (guiHP) {
guiHP.destroy();
guiHP = null;
}
LK.setScore(0);
// Clear persistent stats on full reset
delete storage.selectedParty;
delete storage.collectedOrbs;
startPartySelect();
}
// --- PARTY SELECT ---
function startPartySelect() {
clearCharacterStatsAndReset();
showCharacterStatsAndReset();
gameState = STATE_PARTY_SELECT;
selectedParty = [];
// Show party selection text
partySelectText = new Text2('Choose your party (tap to select 3):', {
size: 90,
fill: '#fff'
});
partySelectText.anchor.set(0.5, 0);
partySelectText.x = 2048 / 2;
partySelectText.y = 180;
game.addChild(partySelectText);
// Show icons for each member
var spacing = 400;
var startX = 2048 / 2 - spacing;
for (var i = 0; i < partyMembers.length; i++) {
var icon = new PartyIcon();
icon.setType(partyMembers[i]);
icon.x = startX + i * spacing;
icon.y = 600;
icon.setSelected(false);
icon.memberId = partyMembers[i];
icon.index = i;
partyIcons.push(icon);
game.addChild(icon);
}
}
// Handle party icon selection
function handlePartySelect(x, y, obj) {
if (gameState !== STATE_PARTY_SELECT) return;
for (var i = 0; i < partyIcons.length; i++) {
var icon = partyIcons[i];
var dx = x - icon.x;
var dy = y - icon.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 80) {
// Toggle selection
var idx = selectedParty.indexOf(icon.memberId);
if (idx === -1 && selectedParty.length < 3) {
selectedParty.push(icon.memberId);
icon.setSelected(true);
} else if (idx !== -1) {
selectedParty.splice(idx, 1);
icon.setSelected(false);
}
// If 3 selected, proceed
if (selectedParty.length === 3) {
// Save party selection persistently
storage.selectedParty = selectedParty.slice();
LK.setTimeout(function () {
for (var j = 0; j < partyIcons.length; j++) partyIcons[j].destroy();
partyIcons = [];
if (partySelectText) {
partySelectText.destroy();
partySelectText = null;
}
startTowerSelect();
}, 400);
}
break;
}
}
}
// --- TOWER SELECT ---
function startTowerSelect() {
clearCharacterStatsAndReset();
showCharacterStatsAndReset();
gameState = STATE_TOWER_SELECT;
// Show tower select text
towerSelectText = new Text2('Choose a tower to conquer:', {
size: 90,
fill: '#fff'
});
towerSelectText.anchor.set(0.5, 0);
towerSelectText.x = 2048 / 2;
towerSelectText.y = 180;
game.addChild(towerSelectText);
// Show towers
var spacing = 350;
var startX = 2048 / 2 - spacing * 2 + spacing / 2;
for (var i = 0; i < towers.length; i++) {
var locked = false;
if (i === 4) {
// Fifth tower locked until all orbs
locked = !(collectedOrbs[0] && collectedOrbs[1] && collectedOrbs[2] && collectedOrbs[3]);
}
var icon = new TowerIcon();
icon.setTower(towers[i].id, locked);
icon.x = startX + i * spacing;
icon.y = 700;
icon.towerIndex = i;
towerIcons.push(icon);
game.addChild(icon);
// Add EX label if in EX mode
if (typeof game !== "undefined" && game._exModeActive) {
var exLabel = new Text2("EX", {
size: 60,
fill: 0xFF3333
});
exLabel.anchor.set(0.5, 0.5);
exLabel.x = icon.x;
exLabel.y = icon.y - 180;
game.addChild(exLabel);
icon._exLabel = exLabel;
}
}
// Show orbs collected
for (var i = 0; i < 4; i++) {
var orb = new OrbIcon();
orb.setOrb(i + 1, collectedOrbs[i]);
orb.x = 2048 / 2 - 200 + i * 130;
orb.y = 1200;
orbIcons.push(orb);
game.addChild(orb);
}
}
// Handle tower icon selection
function handleTowerSelect(x, y, obj) {
if (gameState !== STATE_TOWER_SELECT) return;
for (var i = 0; i < towerIcons.length; i++) {
var icon = towerIcons[i];
var dx = x - icon.x;
var dy = y - (icon.y - 150);
if (dx > -90 && dx < 90 && dy > 0 && dy < 300) {
if (icon.locked) {
// Do nothing
return;
}
// Start battle for this tower
var towerIdx = icon.towerId - 1;
for (var j = 0; j < towerIcons.length; j++) towerIcons[j].destroy();
towerIcons = [];
for (var j = 0; j < orbIcons.length; j++) orbIcons[j].destroy();
orbIcons = [];
if (towerSelectText) {
towerSelectText.destroy();
towerSelectText = null;
}
startBattle(towerIdx);
break;
}
}
}
// --- BATTLE ---
function startBattle(towerIdx) {
clearCharacterStatsAndReset();
gameState = STATE_BATTLE;
battleActive = true;
battleWin = false;
battleLose = false;
battlePhase = 0;
battleTimer = 0;
battleBossId = towerIdx + 1;
if (typeof game !== "undefined" && game._exModeActive) {
// EX mode: more HP
battleBossHP = (40 + towerIdx * 20) * 2.2;
} else {
battleBossHP = 40 + towerIdx * 20;
}
battleBossMaxHP = battleBossHP;
battleBullets = [];
// Initialize each party member's HP
partyCurrentHP = [];
for (var i = 0; i < selectedParty.length; i++) {
var type = selectedParty[i];
partyCurrentHP[i] = characterStats[type].hp;
}
// --- MINOR ENEMY FIGHTS SETUP ---
var minorEnemies = [];
var minorEnemyCount = 2 + towerIdx; // 2,3,4,5,6 for each tower
var minorEnemyHP = 16 + towerIdx * 6;
var minorEnemyIdx = 0;
var inMinorFight = true;
var minorEnemyObj = null;
var minorEnemyText = null;
var minorEnemyHPText = null;
var minorEnemyPatternTimer = 0;
var minorEnemyPatternId = 0;
var minorEnemyNames = ["Goblin", "Slime", "Bat", "Imp", "Wisp", "Rat", "Mimic", "Sprite"];
var minorEnemyClasses = [GoblinEnemy, SlimeEnemy, BatEnemy, ImpEnemy, WispEnemy, RatEnemy, MimicEnemy, SpriteEnemy];
var minorEnemyName = "";
var minorEnemyClass = GoblinEnemy;
// Start with minor enemy fight before boss
inMinorFight = true;
minorEnemyIdx = 0;
// Draw white box for heart movement area
if (typeof heartBox !== "undefined" && heartBox) {
heartBox.destroy();
heartBox = null;
}
var heartBox = new Container();
var boxMinX = 2048 / 2 - 350;
var boxMaxX = 2048 / 2 + 350;
var boxMinY = 1200;
var boxMaxY = 2200;
var boxW = boxMaxX - boxMinX;
var boxH = boxMaxY - boxMinY;
var borderW = 8;
var boxColor = 0xffffff;
// Top border
var topLine = LK.getAsset('bullet', {
width: boxW,
height: borderW,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
topLine.x = boxMinX;
topLine.y = boxMinY;
heartBox.addChild(topLine);
// Bottom border
var bottomLine = LK.getAsset('bullet', {
width: boxW,
height: borderW,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
bottomLine.x = boxMinX;
bottomLine.y = boxMaxY - borderW;
heartBox.addChild(bottomLine);
// Left border
var leftLine = LK.getAsset('bullet', {
width: borderW,
height: boxH,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
leftLine.x = boxMinX;
leftLine.y = boxMinY;
heartBox.addChild(leftLine);
// Right border
var rightLine = LK.getAsset('bullet', {
width: borderW,
height: boxH,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
rightLine.x = boxMaxX - borderW;
rightLine.y = boxMinY;
heartBox.addChild(rightLine);
game.addChild(heartBox);
// Show heart
heart = new Heart();
heart.x = 2048 / 2;
heart.y = 1800;
heartStartX = heart.x;
heartStartY = heart.y;
game.addChild(heart);
// Show party in GUI
for (var i = 0; i < selectedParty.length; i++) {
var icon = new PartyIcon();
icon.setType(selectedParty[i]);
icon.x = 200 + i * 150;
icon.y = 100;
icon.setSelected(true);
guiParty.push(icon);
LK.gui.top.addChild(icon);
}
// Show orbs in GUI
for (var i = 0; i < 4; i++) {
var orb = new OrbIcon();
orb.setOrb(i + 1, collectedOrbs[i]);
orb.x = 2048 - 200 - (3 - i) * 120;
orb.y = 100;
guiOrbs.push(orb);
LK.gui.top.addChild(orb);
}
// Show HP in GUI
// Show each party member's HP
if (guiHP) {
guiHP.destroy();
guiHP = null;
}
guiHP = [];
for (var i = 0; i < selectedParty.length; i++) {
var hpText = new Text2('❤ ' + partyCurrentHP[i], {
size: 80,
fill: '#ff3366'
});
hpText.anchor.set(0, 0);
hpText.x = 120 + i * 220;
hpText.y = 220;
LK.gui.top.addChild(hpText);
guiHP.push(hpText);
}
// Show orbs
battleOrbsText = new Text2('Orbs: ' + getOrbsCount() + '/4', {
size: 60,
fill: '#fff'
});
battleOrbsText.anchor.set(0.5, 0);
battleOrbsText.x = 2048 / 2;
battleOrbsText.y = 450;
game.addChild(battleOrbsText);
// --- ENEMY ATTACK DELAY STATE ---
var enemyAttackDelayTicks = 180; // 3 seconds at 60fps
var enemyAttackDelayActive = true;
// --- PLAYER TURN MENU REMOVED ---
// (player turn menu, shop, and magic menu functions removed)
var shopMenu = null;
var shopActive = false;
// --- MINOR ENEMY FIGHT LOGIC ---
function startMinorEnemyFight(idx) {
// Clean up previous
if (minorEnemyObj) {
minorEnemyObj.destroy();
minorEnemyObj = null;
}
if (minorEnemyText) {
minorEnemyText.destroy();
minorEnemyText = null;
}
if (minorEnemyHPText) {
minorEnemyHPText.destroy();
minorEnemyHPText = null;
}
minorEnemyPatternTimer = 0;
minorEnemyPatternId = idx % 4;
minorEnemyName = minorEnemyNames[idx % minorEnemyNames.length];
minorEnemyClass = minorEnemyClasses[idx % minorEnemyClasses.length];
// Create enemy
minorEnemyObj = new minorEnemyClass();
minorEnemyObj.hp = minorEnemyHP;
minorEnemyObj.maxHP = minorEnemyHP;
minorEnemyObj.x = 2048 / 2;
minorEnemyObj.y = 700;
game.addChild(minorEnemyObj);
// Name
minorEnemyText = new Text2(minorEnemyName, {
size: 90,
fill: '#fff'
});
minorEnemyText.anchor.set(0.5, 0);
minorEnemyText.x = 2048 / 2;
minorEnemyText.y = 200;
game.addChild(minorEnemyText);
// HP
minorEnemyHPText = new Text2('HP: ' + minorEnemyObj.hp, {
size: 70,
fill: '#fff'
});
minorEnemyHPText.anchor.set(0.5, 0);
minorEnemyHPText.x = 2048 / 2;
minorEnemyHPText.y = 320;
game.addChild(minorEnemyHPText);
// Reset enemy attack delay for each new minor enemy
enemyAttackDelayTicks = 180;
enemyAttackDelayActive = true;
}
function cleanupMinorEnemyFight() {
if (minorEnemyObj) {
minorEnemyObj.destroy();
minorEnemyObj = null;
}
if (minorEnemyText) {
minorEnemyText.destroy();
minorEnemyText = null;
}
if (minorEnemyHPText) {
minorEnemyHPText.destroy();
minorEnemyHPText = null;
}
for (var i = battleBullets.length - 1; i >= 0; i--) {
battleBullets[i].destroy();
battleBullets.splice(i, 1);
}
LK.setScore(0);
}
function updateMinorEnemyFight() {
if (!minorEnemyObj) return;
// Update heart
if (heart) heart.update();
// Update bullets
for (var i = battleBullets.length - 1; i >= 0; i--) {
var b = battleBullets[i];
b.update();
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
// Heart bullet destroys enemy bullet
if (b instanceof HeartBullet) {
// Check collision with enemy bullets
for (var j = battleBullets.length - 1; j >= 0; j--) {
var eb = battleBullets[j];
if (eb === b) continue;
if (eb instanceof EnemyBullet && eb.active && b.active && circleIntersect(b.x, b.y, b.radius, eb.x, eb.y, eb.radius)) {
eb.active = false;
eb.destroy();
b.active = false;
b.destroy();
battleBullets.splice(j, 1);
break;
}
}
// Heart bullet hits minor enemy
if (minorEnemyObj && b.active && circleIntersect(b.x, b.y, b.radius, minorEnemyObj.x, minorEnemyObj.y, 60)) {
minorEnemyObj.hp -= 0.5;
if (minorEnemyObj.hp < 0) minorEnemyObj.hp = 0;
if (minorEnemyHPText) minorEnemyHPText.setText('HP: ' + Math.max(0, Math.round(minorEnemyObj.hp * 10) / 10));
LK.getSound('hit').play();
b.active = false;
b.destroy();
}
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
}
// Collision with heart
if (!heart.invincible && b instanceof EnemyBullet && circleIntersect(heart.x, heart.y, heart.radius, b.x, b.y, b.radius)) {
heart.setInvincible(40);
LK.effects.flashObject(heart, 0xff0000, 400);
LK.getSound('hit').play();
// Randomly select a party member to take damage
if (partyCurrentHP.length > 0) {
var targetIdx = Math.floor(Math.random() * partyCurrentHP.length);
partyCurrentHP[targetIdx] -= 1;
if (partyCurrentHP[targetIdx] < 0) partyCurrentHP[targetIdx] = 0;
if (guiHP && guiHP[targetIdx]) guiHP[targetIdx].setText('❤ ' + partyCurrentHP[targetIdx]);
}
// Lose if all party members are at 0 HP
var allDead = true;
for (var i = 0; i < partyCurrentHP.length; i++) {
if (partyCurrentHP[i] > 0) allDead = false;
}
if (allDead) {
battleLose = true;
battleActive = false;
endBattle(false);
return;
}
}
}
// --- ENEMY ATTACK DELAY LOGIC ---
if (enemyAttackDelayActive) {
enemyAttackDelayTicks--;
if (enemyAttackDelayTicks <= 0) {
enemyAttackDelayActive = false;
}
return; // Don't attack until delay is over
}
// Enemy attack pattern
minorEnemyPatternTimer++;
if (minorEnemyPatternTimer % 40 === 0) {
spawnBulletPattern(minorEnemyPatternId);
// Minor enemy HP drops a bit each pattern
minorEnemyObj.hp--;
if (minorEnemyObj.hp < 0) minorEnemyObj.hp = 0;
if (minorEnemyHPText) minorEnemyHPText.setText('HP: ' + minorEnemyObj.hp);
}
// Win condition: survive or reduce enemy HP
if (minorEnemyObj.hp <= 0) {
// Next enemy or boss
cleanupMinorEnemyFight();
minorEnemyIdx++;
if (minorEnemyIdx < minorEnemyCount) {
startMinorEnemyFight(minorEnemyIdx);
} else {
// Proceed to boss
inMinorFight = false;
startBossFight();
}
}
}
function startBossFight() {
// Clean up minor enemy UI
if (minorEnemyObj) {
minorEnemyObj.destroy();
minorEnemyObj = null;
}
if (minorEnemyText) {
minorEnemyText.destroy();
minorEnemyText = null;
}
if (minorEnemyHPText) {
minorEnemyHPText.destroy();
minorEnemyHPText = null;
}
for (var i = battleBullets.length - 1; i >= 0; i--) {
battleBullets[i].destroy();
battleBullets.splice(i, 1);
}
LK.setScore(0);
// Show boss name and image
var bossNames = ['Amethyst Guardian', 'Emerald Warden', 'Amber Sentinel', 'Crimson Overlord', 'Obsidian Nemesis'];
var bossClasses = [AmethystBoss, EmeraldBoss, AmberBoss, CrimsonBoss, ObsidianBoss];
battleBossText = new Text2(bossNames[towerIdx], {
size: 100,
fill: '#fff'
});
battleBossText.anchor.set(0.5, 0);
battleBossText.x = 2048 / 2;
battleBossText.y = 200;
game.addChild(battleBossText);
// Show boss image
if (typeof battleBossSprite !== "undefined" && battleBossSprite) {
battleBossSprite.destroy();
battleBossSprite = null;
}
battleBossSprite = new bossClasses[towerIdx]();
battleBossSprite.x = 2048 / 2;
battleBossSprite.y = 500;
game.addChild(battleBossSprite);
// Show HP
battleHPText = new Text2('HP: ' + battleBossHP, {
size: 80,
fill: '#fff'
});
battleHPText.anchor.set(0.5, 0);
battleHPText.x = 2048 / 2;
battleHPText.y = 350;
game.addChild(battleHPText);
LK.setScore(0);
battleTimer = 0;
battlePhase = 0;
battleActive = true;
// Sword event variables
battleSwordEvent = null;
battleSwordEventTimer = 0;
// Reset enemy attack delay for boss
enemyAttackDelayTicks = 180;
enemyAttackDelayActive = true;
}
// Start music
if (towerIdx === 4) {
LK.playMusic('boss_theme');
} else {
LK.playMusic('battle_theme');
}
// Start first minor enemy
startMinorEnemyFight(0);
// Patch updateBattle to handle minor enemies before boss
var _originalUpdateBattle = updateBattle;
updateBattle = function updateBattle() {
if (gameState !== STATE_BATTLE || !battleActive) return;
if (inMinorFight) {
updateMinorEnemyFight();
return;
}
// Boss fight as before
// (player turn menu logic removed)
// Update heart
if (heart) heart.update();
// Update bullets
for (var i = battleBullets.length - 1; i >= 0; i--) {
var b = battleBullets[i];
b.update();
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
// Collision with heart
if (!heart.invincible && circleIntersect(heart.x, heart.y, heart.radius, b.x, b.y, b.radius)) {
heart.setInvincible(40);
LK.effects.flashObject(heart, 0xff0000, 400);
LK.getSound('hit').play();
// Randomly select a party member to take damage
if (partyCurrentHP.length > 0) {
var targetIdx = Math.floor(Math.random() * partyCurrentHP.length);
partyCurrentHP[targetIdx] -= 1;
if (partyCurrentHP[targetIdx] < 0) partyCurrentHP[targetIdx] = 0;
if (guiHP && guiHP[targetIdx]) guiHP[targetIdx].setText('❤ ' + partyCurrentHP[targetIdx]);
}
// Lose if all party members are at 0 HP
var allDead = true;
for (var i = 0; i < partyCurrentHP.length; i++) {
if (partyCurrentHP[i] > 0) allDead = false;
}
if (allDead) {
battleLose = true;
battleActive = false;
endBattle(false);
return;
}
}
}
// --- ENEMY ATTACK DELAY LOGIC ---
if (enemyAttackDelayActive) {
enemyAttackDelayTicks--;
if (enemyAttackDelayTicks <= 0) {
enemyAttackDelayActive = false;
}
return; // Don't attack until delay is over
}
// Boss attack pattern
battleTimer++;
if (battleBossId === 5) {
// Final boss: mix of all patterns
if (battleTimer % 30 === 0) {
spawnBulletPattern(battleTimer / 300 % 4 | 0);
}
} else {
if (battleTimer % 40 === 0) {
spawnBulletPattern(battleBossId - 1);
}
}
// --- Sword Event Logic ---
if (typeof battleSwordEventTimer === "undefined") battleSwordEventTimer = 0;
if (typeof battleSwordEvent === "undefined") battleSwordEvent = null;
// Only spawn sword event if boss fight is active
if (battleActive) {
// If no sword event, randomly spawn one every 180-300 ticks
if (!battleSwordEvent || !battleSwordEvent.active) {
battleSwordEventTimer++;
// Randomly spawn between 180 and 300 ticks
if (battleSwordEventTimer > 180 + Math.floor(Math.random() * 120)) {
battleSwordEventTimer = 0;
battleSwordEvent = new SwordEvent();
// Place sword randomly in heart box area, but always on the same row as the heart
var minX = 2048 / 2 - 300,
maxX = 2048 / 2 + 300;
battleSwordEvent.x = minX + Math.random() * (maxX - minX);
// Place sword at the same y as the heart (heart.y), or default to 1800 if heart is missing
battleSwordEvent.y = heart && typeof heart.y === "number" ? heart.y : 1800;
game.addChild(battleSwordEvent);
}
}
// Update sword event if present
if (battleSwordEvent && battleSwordEvent.active) {
battleSwordEvent.update();
// If heart touches sword, deal damage to boss and destroy sword
if (heart && !heart.invincible && circleIntersect(heart.x, heart.y, heart.radius, battleSwordEvent.x, battleSwordEvent.y, battleSwordEvent.radius)) {
battleBossHP -= 5;
if (battleBossHP < 0) battleBossHP = 0;
if (battleHPText) battleHPText.setText('HP: ' + Math.round(battleBossHP * 10) / 10);
LK.getSound('hit').play();
battleSwordEvent.active = false;
battleSwordEvent.destroy();
}
}
}
// Win condition: survive for N ticks or reduce boss HP
if (battleBossHP <= 0) {
battleWin = true;
battleActive = false;
endBattle(true);
return;
}
};
}
// Get orbs count
function getOrbsCount() {
var c = 0;
for (var i = 0; i < collectedOrbs.length; i++) if (collectedOrbs[i]) c++;
return c;
}
// Handle heart drag
function handleHeartDrag(x, y, obj) {
if (gameState !== STATE_BATTLE || !battleActive) return;
if (!heart) return;
// Clamp to battle box
var minX = 2048 / 2 - 350,
maxX = 2048 / 2 + 350;
var minY = 1200,
maxY = 2200;
var prevX = heart.x;
var prevY = heart.y;
heart.x = Math.max(minX, Math.min(maxX, x));
heart.y = Math.max(minY, Math.min(maxY, y));
// Shoot yellow bullet if moved (dragged)
if (typeof heart.lastShootTick === "undefined") heart.lastShootTick = 0;
if (typeof heartShootCooldown === "undefined") heartShootCooldown = 4;
if (Math.abs(heart.x - prevX) > 2 || Math.abs(heart.y - prevY) > 2) {
// Only shoot if enough ticks have passed since last shot
if (typeof LK !== "undefined" && LK.ticks - heart.lastShootTick > heartShootCooldown) {
var b = new HeartBullet();
b.init(heart.x, heart.y - heart.radius - 10);
if (typeof battleBullets !== "undefined") {
battleBullets.push(b);
}
if (typeof game !== "undefined") {
game.addChild(b);
}
heart.lastShootTick = LK.ticks;
}
}
}
// Handle heart down
function handleHeartDown(x, y, obj) {
if (gameState !== STATE_BATTLE || !battleActive) return;
if (!heart) return;
var dx = x - heart.x;
var dy = y - heart.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < heart.radius + 10) {
heartDragging = true;
heartDragOffsetX = dx;
heartDragOffsetY = dy;
}
}
// Handle heart up
function handleHeartUp(x, y, obj) {
if (gameState !== STATE_BATTLE || !battleActive) return;
heartDragging = false;
}
// --- BATTLE UPDATE ---
function updateBattle() {
if (gameState !== STATE_BATTLE || !battleActive) return;
// Update heart
if (heart) heart.update();
// Update bullets
for (var i = battleBullets.length - 1; i >= 0; i--) {
var b = battleBullets[i];
b.update();
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
// Heart bullet destroys enemy bullet
if (b instanceof HeartBullet) {
// Check collision with enemy bullets
for (var j = battleBullets.length - 1; j >= 0; j--) {
var eb = battleBullets[j];
if (eb === b) continue;
// Only destroy enemy bullets (not other heart bullets)
if (eb instanceof EnemyBullet && eb.active && b.active && circleIntersect(b.x, b.y, b.radius, eb.x, eb.y, eb.radius)) {
if (typeof eb.hp === "number" && eb.hp > 1) {
eb.hp--;
b.active = false;
b.destroy();
// Play hit sound for feedback
LK.getSound('hit').play();
// If hp drops to 0, destroy
if (eb.hp <= 0) {
eb.active = false;
eb.destroy();
battleBullets.splice(j, 1);
}
} else {
eb.active = false;
eb.destroy();
b.active = false;
b.destroy();
// Remove both from array
battleBullets.splice(j, 1);
}
break;
}
}
// Heart bullet hits boss
if (battleBossSprite && b.active && circleIntersect(b.x, b.y, b.radius, battleBossSprite.x, battleBossSprite.y, 90)) {
battleBossHP -= 0.5;
if (battleBossHP < 0) battleBossHP = 0;
if (battleHPText) battleHPText.setText('HP: ' + Math.round(battleBossHP * 10) / 10);
LK.getSound('hit').play();
b.active = false;
b.destroy();
}
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
}
// Collision with heart
if (!heart.invincible && b instanceof EnemyBullet && circleIntersect(heart.x, heart.y, heart.radius, b.x, b.y, b.radius)) {
// Hit!
heart.setInvincible(40);
LK.effects.flashObject(heart, 0xff0000, 400);
LK.getSound('hit').play();
LK.setScore(LK.getScore() + 1);
// Lose if hit 3 times
if (LK.getScore() >= 3) {
battleLose = true;
battleActive = false;
endBattle(false);
return;
}
}
}
// Boss attack pattern
battleTimer++;
if (battleBossId === 5) {
// Final boss: mix of all patterns
if (battleTimer % 30 === 0) {
spawnBulletPattern(battleTimer / 300 % 4 | 0);
}
} else {
if (battleTimer % 40 === 0) {
spawnBulletPattern(battleBossId - 1);
}
}
// Win condition: survive for N ticks or reduce boss HP
if (battleBossHP <= 0) {
battleWin = true;
battleActive = false;
endBattle(true);
return;
}
}
// --- BATTLE PATTERNS ---
function spawnBulletPattern(patternId) {
var cx = 2048 / 2,
cy = 1700;
// Determine EX mode bullet speed multiplier
var exSpeed = typeof game !== "undefined" && game._exModeActive ? 1.6 : 1;
if (patternId === 0) {
// Simple downward rain
for (var i = 0; i < 5; i++) {
var b = new EnemyBullet();
b.init(cx - 300 + 150 * i, 1200, 0, 12 * exSpeed, true); // true = boss fight bullet
battleBullets.push(b);
game.addChild(b);
}
} else if (patternId === 1) {
// Sine wave
for (var i = 0; i < 4; i++) {
var b = new EnemyBullet();
b.init(cx - 200 + 200 * i, 1200, Math.sin(LK.ticks / 20 + i) * 6 * exSpeed, 10 * exSpeed, true);
battleBullets.push(b);
game.addChild(b);
}
} else if (patternId === 2) {
// Spiral
var angle = LK.ticks % 360 / 180 * Math.PI;
for (var i = 0; i < 6; i++) {
var a = angle + i * Math.PI / 3;
var b = new EnemyBullet();
b.init(cx, 1400, Math.cos(a) * 8 * exSpeed, (Math.sin(a) * 8 + 8) * exSpeed, true);
battleBullets.push(b);
game.addChild(b);
}
} else if (patternId === 3) {
// Random bursts
for (var i = 0; i < 8; i++) {
var a = Math.random() * Math.PI * 2;
var b = new EnemyBullet();
b.init(cx, 1500, Math.cos(a) * 10 * exSpeed, (Math.sin(a) * 10 + 10) * exSpeed, true);
battleBullets.push(b);
game.addChild(b);
}
}
// Boss HP drops a bit each pattern
battleBossHP--;
if (battleBossHP < 0) battleBossHP = 0;
if (battleHPText) battleHPText.setText('HP: ' + Math.round(battleBossHP * 10) / 10);
}
// --- END BATTLE ---
function endBattle(win) {
if (win) {
LK.getSound('boss_defeat').play();
// Collect orb if not final boss
if (battleBossId <= 4) {
collectedOrbs[battleBossId - 1] = true;
// Save orbs persistently
storage.collectedOrbs = collectedOrbs.slice();
LK.getSound('collect_orb').play();
}
// Show win text
var winText = new Text2('Victory!', {
size: 140,
fill: '#fff'
});
winText.anchor.set(0.5, 0.5);
winText.x = 2048 / 2;
winText.y = 1200;
game.addChild(winText);
LK.effects.flashScreen(0x00ff00, 800);
battleEndTimeout = LK.setTimeout(function () {
winText.destroy();
cleanupBattle();
if (battleBossId === 5) {
// Game complete
showGameComplete();
} else {
startTowerSelect();
}
}, 1800);
} else {
// Lose
var loseText = new Text2('Defeated!', {
size: 140,
fill: '#fff'
});
loseText.anchor.set(0.5, 0.5);
loseText.x = 2048 / 2;
loseText.y = 1200;
game.addChild(loseText);
LK.effects.flashScreen(0xff0000, 800);
battleEndTimeout = LK.setTimeout(function () {
loseText.destroy();
cleanupBattle();
startTowerSelect();
}, 1800);
}
}
// --- CLEANUP BATTLE ---
function cleanupBattle() {
if (heart) {
heart.destroy();
heart = null;
}
// Remove heartBox if present
if (typeof heartBox !== "undefined" && heartBox) {
heartBox.destroy();
heartBox = null;
}
for (var i = 0; i < battleBullets.length; i++) battleBullets[i].destroy();
battleBullets = [];
if (battleBossText) {
battleBossText.destroy();
battleBossText = null;
}
// Remove sword event if present
if (typeof battleSwordEvent !== "undefined" && battleSwordEvent) {
battleSwordEvent.destroy();
battleSwordEvent = null;
}
// Remove boss sprite if present
if (typeof battleBossSprite !== "undefined" && battleBossSprite) {
battleBossSprite.destroy();
battleBossSprite = null;
}
if (battleHPText) {
battleHPText.destroy();
battleHPText = null;
}
if (battleOrbsText) {
battleOrbsText.destroy();
battleOrbsText = null;
}
for (var i = 0; i < guiParty.length; i++) guiParty[i].destroy();
guiParty = [];
for (var i = 0; i < guiOrbs.length; i++) guiOrbs[i].destroy();
guiOrbs = [];
if (guiHP) {
if (Array.isArray(guiHP)) {
for (var i = 0; i < guiHP.length; i++) {
if (guiHP[i]) guiHP[i].destroy();
}
} else {
guiHP.destroy();
}
guiHP = null;
}
LK.setScore(0);
LK.stopMusic();
}
// --- GAME COMPLETE ---
function showGameComplete() {
clearCharacterStatsAndReset();
gameState = STATE_GAME_COMPLETE;
var completeText = new Text2('You conquered all towers!\nQuest Complete!', {
size: 120,
fill: '#fff'
});
completeText.anchor.set(0.5, 0.5);
completeText.x = 2048 / 2;
completeText.y = 1200;
game.addChild(completeText);
LK.effects.flashScreen(0x00ffcc, 1200);
LK.setTimeout(function () {
completeText.destroy();
// Offer EX Tower mode if not already in EX mode
if (!game._exModeActive) {
// Show EX mode prompt
var exText = new Text2('Secret EX Towers unlocked!\nTap to challenge EX Towers!', {
size: 100,
fill: '#ff3333'
});
exText.anchor.set(0.5, 0.5);
exText.x = 2048 / 2;
exText.y = 1200;
game.addChild(exText);
// Set up tap to start EX mode
var _exModeHandler = function exModeHandler(x, y, obj) {
if (exText && exText.parent) {
var bx = exText.x - exText.width * exText.anchor.x;
var by = exText.y - exText.height * exText.anchor.y;
var bw = exText.width;
var bh = exText.height;
if (x >= bx && x <= bx + bw && y >= by && y <= by + bh) {
exText.destroy();
game.off('down', _exModeHandler);
startEXMode();
}
}
};
game.on('down', _exModeHandler);
} else {
// If already in EX mode, reset to normal game
resetGame();
}
}, 3000);
}
// --- EX TOWER MODE ---
function startEXMode() {
// Mark EX mode active
game._exModeActive = true;
// Reset all state, but mark EX mode
gameState = STATE_TOWER_SELECT;
selectedParty = [];
// Keep orbs and party, but reset towers
for (var i = 0; i < partyIcons.length; i++) partyIcons[i].destroy();
partyIcons = [];
for (var i = 0; i < towerIcons.length; i++) towerIcons[i].destroy();
towerIcons = [];
for (var i = 0; i < orbIcons.length; i++) orbIcons[i].destroy();
orbIcons = [];
if (partySelectText) {
partySelectText.destroy();
partySelectText = null;
}
if (towerSelectText) {
towerSelectText.destroy();
towerSelectText = null;
}
if (heart) {
heart.destroy();
heart = null;
}
for (var i = 0; i < battleBullets.length; i++) battleBullets[i].destroy();
battleBullets = [];
if (battleBossText) {
battleBossText.destroy();
battleBossText = null;
}
if (battleHPText) {
battleHPText.destroy();
battleHPText = null;
}
if (battleOrbsText) {
battleOrbsText.destroy();
battleOrbsText = null;
}
if (battleEndTimeout) {
LK.clearTimeout(battleEndTimeout);
battleEndTimeout = null;
}
for (var i = 0; i < guiParty.length; i++) guiParty[i].destroy();
guiParty = [];
for (var i = 0; i < guiOrbs.length; i++) guiOrbs[i].destroy();
guiOrbs = [];
if (guiHP) {
if (Array.isArray(guiHP)) {
for (var i = 0; i < guiHP.length; i++) {
if (guiHP[i]) guiHP[i].destroy();
}
} else {
guiHP.destroy();
}
guiHP = null;
}
LK.setScore(0);
// Show EX tower select
startTowerSelect();
}
// --- CIRCLE INTERSECT ---
function circleIntersect(x1, y1, r1, x2, y2, r2) {
var dx = x1 - x2,
dy = y1 - y2;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < r1 + r2 - 10;
}
// --- GAME EVENTS ---
game.down = function (x, y, obj) {
// Check for reset button tap
if (resetButton && resetButton.parent) {
var btn = resetButton;
var bx = btn.x - btn.width * btn.anchor.x;
var by = btn.y - btn.height * btn.anchor.y;
var bw = btn.width;
var bh = btn.height;
// Convert to LK.gui.bottom coordinates
var local = LK.gui.bottom.toLocal({
x: x,
y: y
});
if (local.x >= bx && local.x <= bx + bw && local.y >= by && local.y <= by + bh) {
resetGame();
return;
}
}
// (player turn menu and shop/magic menu input handling removed)
if (gameState === STATE_PARTY_SELECT) {
handlePartySelect(x, y, obj);
} else if (gameState === STATE_TOWER_SELECT) {
handleTowerSelect(x, y, obj);
} else if (gameState === STATE_BATTLE && battleActive) {
handleHeartDown(x, y, obj);
}
};
game.move = function (x, y, obj) {
if (gameState === STATE_BATTLE && battleActive && heartDragging) {
handleHeartDrag(x - heartDragOffsetX, y - heartDragOffsetY, obj);
}
};
game.up = function (x, y, obj) {
if (gameState === STATE_BATTLE && battleActive) {
handleHeartUp(x, y, obj);
}
};
// --- GAME UPDATE ---
game.update = function () {
if (gameState === STATE_BATTLE && battleActive) {
updateBattle();
}
};
// --- START GAME ---
LK.playMusic('main_theme');
resetGame();
// Show username at the bottom of the screen
if (typeof usernameText !== "undefined" && usernameText) {
usernameText.destroy();
usernameText = null;
}
var username = storage.username || "Player";
var usernameText = new Text2(username, {
size: 60,
fill: "#fff"
});
usernameText.anchor.set(0.5, 1);
usernameText.x = 2048 / 2;
usernameText.y = 2732 - 20;
LK.gui.bottom.addChild(usernameText); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var AmberBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 70;
self.maxHP = 70;
self.exp = 70;
self.sprite = self.attachAsset('boss_amber', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Boss Classes
var AmethystBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 50;
self.maxHP = 50;
self.exp = 50;
self.sprite = self.attachAsset('boss_amethyst', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var BatEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_bat', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var CrimsonBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 80;
self.maxHP = 80;
self.exp = 80;
self.sprite = self.attachAsset('boss_crimson', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var EmeraldBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 60;
self.maxHP = 60;
self.exp = 60;
self.sprite = self.attachAsset('boss_emerald', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Enemy Bullet
var EnemyBullet = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('enemy_bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.vx = 0;
self.vy = 0;
self.radius = 30;
self.active = true;
self.hp = 1; // Default, will be set to 2 for boss fight bullets
self.init = function (x, y, vx, vy, isBossFight) {
self.x = x;
self.y = y;
self.vx = vx;
self.vy = vy;
self.hp = isBossFight ? 2 : 1;
};
self.update = function () {
if (!self.active) return;
self.x += self.vx;
self.y += self.vy;
// Offscreen check
if (self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 2832) {
self.active = false;
self.destroy();
}
};
return self;
});
// Minor Enemy Classes
var GoblinEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Heart (player dodge object)
var Heart = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 40;
self.invincible = false;
self.invincibleTicks = 0;
self.setInvincible = function (ticks) {
self.invincible = true;
self.invincibleTicks = ticks;
self.sprite.alpha = 0.5;
};
self.update = function () {
if (self.invincible) {
self.invincibleTicks--;
if (self.invincibleTicks <= 0) {
self.invincible = false;
self.sprite.alpha = 1;
}
}
};
return self;
});
// HeartBullet (player's yellow bullet)
var HeartBullet = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xb8b031,
// yellow
width: 60,
height: 60,
shape: 'box'
});
self.radius = 30;
self.vx = 0;
self.vy = -18;
self.active = true;
self.init = function (x, y) {
self.x = x;
self.y = y;
};
self.update = function () {
if (!self.active) return;
self.y += self.vy;
// Offscreen check
if (self.y < -100) {
self.active = false;
self.destroy();
}
};
return self;
});
var ImpEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_imp', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var MimicEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
// Randomly select a mimic sprite variant
var mimicVariants = ['enemy_mimic', 'enemy_mimic2', 'enemy_mimic3'];
var idx = Math.floor(Math.random() * mimicVariants.length);
self.sprite = self.attachAsset(mimicVariants[idx], {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var ObsidianBoss = Container.expand(function () {
var self = Container.call(this);
self.hp = 100;
self.maxHP = 100;
self.exp = 100;
self.sprite = self.attachAsset('boss_obsidian', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// Orb Icon
var OrbIcon = Container.expand(function () {
var self = Container.call(this);
self.orbId = 1;
self.collected = false;
self.icon = null;
self.setOrb = function (orbId, collected) {
self.orbId = orbId;
self.collected = collected;
if (self.icon) self.icon.destroy();
var assetId = 'orb' + orbId;
self.icon = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.icon.alpha = collected ? 1 : 0.3;
};
return self;
});
// Party Member Icon
var PartyIcon = Container.expand(function () {
var self = Container.call(this);
self.type = null;
self.selected = false;
self.icon = null;
self.setType = function (type) {
self.type = type;
if (self.icon) self.icon.destroy();
var assetId = '';
if (type === 'white_mage') assetId = 'icon_white_mage';
if (type === 'thief') assetId = 'icon_thief';
if (type === 'martial') assetId = 'icon_martial';
if (type === 'knight') assetId = 'icon_knight';
self.icon = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.setSelected = function (selected) {
self.selected = selected;
if (self.icon) {
self.icon.alpha = selected ? 1 : 0.6;
self.icon.scaleX = self.icon.scaleY = selected ? 1.15 : 1;
}
};
return self;
});
var RatEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_rat', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var SlimeEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_slime', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
var SpriteEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_sprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
// SwordEvent: sword that appears randomly during boss fight
var SwordEvent = Container.expand(function () {
var self = Container.call(this);
self.sprite = self.attachAsset('sword_event', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 60;
self.active = true;
self.vanishTicks = 120; // lasts for 2 seconds
self.update = function () {
if (!self.active) return;
self.vanishTicks--;
if (self.vanishTicks <= 0) {
self.active = false;
self.destroy();
}
};
return self;
});
// Tower Icon
var TowerIcon = Container.expand(function () {
var self = Container.call(this);
self.towerId = 1;
self.locked = false;
self.icon = null;
self.setTower = function (towerId, locked) {
self.towerId = towerId;
self.locked = locked;
if (self.icon) self.icon.destroy();
var assetId = 'tower' + towerId;
self.icon = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 1
});
// Tint red if EX mode is active
if (typeof game !== "undefined" && game._exModeActive) {
// Use tween for instant tint
tween(self.icon, {
tint: 0xFF3333
}, {
duration: 0
});
}
self.icon.alpha = locked ? 0.4 : 1;
};
return self;
});
var WispEnemy = Container.expand(function () {
var self = Container.call(this);
self.hp = 10;
self.maxHP = 10;
self.exp = 3;
self.sprite = self.attachAsset('enemy_wisp', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222233
});
/****
* Game Code
****/
// sword image for boss event
// --- PLAYER TURN MENU REMOVED ---
// (player turn menu and related variables removed)
var shopMenu = null; // Ensure shopMenu is defined globally
var shopActive = false; // Ensure shopActive is defined globally
// Enemy images
// --- STORAGE PLUGIN ---
// Music
// Sound effects
// Orb icons
// Tower icons
// Enemy bullet
// Heart (player dodge object)
// Party member icons
// Game state
var STATE_PARTY_SELECT = 0;
var STATE_TOWER_SELECT = 1;
var STATE_BATTLE = 2;
var STATE_BOSS_DEFEAT = 3;
var STATE_GAME_COMPLETE = 4;
var gameState = STATE_PARTY_SELECT;
// Party selection
var partyMembers = ['white_mage', 'thief', 'martial', 'knight'];
// Load persistent party and orbs if available
var selectedParty = [];
var collectedOrbs = [false, false, false, false];
(function () {
var savedParty = storage.selectedParty;
var savedOrbs = storage.collectedOrbs;
if (savedParty && Array.isArray(savedParty) && savedParty.length === 3) {
selectedParty = savedParty.slice();
}
if (savedOrbs && Array.isArray(savedOrbs) && savedOrbs.length === 4) {
collectedOrbs = savedOrbs.slice();
}
})();
var partyIcons = [];
var partySelectText = null;
// Tower selection
var towers = [{
id: 1,
name: 'Amethyst Tower',
orb: 1
}, {
id: 2,
name: 'Emerald Tower',
orb: 2
}, {
id: 3,
name: 'Amber Tower',
orb: 3
}, {
id: 4,
name: 'Crimson Tower',
orb: 4
}, {
id: 5,
name: 'Obsidian Tower',
orb: 0
}];
var towerIcons = [];
var orbIcons = [];
var towerSelectText = null;
// Battle
var heart = null;
var heartStartX = 0;
var heartStartY = 0;
var heartDragging = false;
var heartDragOffsetX = 0;
var heartDragOffsetY = 0;
var battleBullets = [];
var battleTimer = 0;
var battlePhase = 0;
var battleBoss = null;
var battleBossId = 0;
var battleBossHP = 0;
var battleBossMaxHP = 0;
var battleBossText = null;
var battleHPText = null;
var battleOrbsText = null;
var battleActive = false;
var battleWin = false;
var battleLose = false;
var battleEndTimeout = null;
// GUI
var guiParty = [];
var guiOrbs = [];
var guiHP = null;
// --- CHARACTER STATS ---
var characterStats = {
white_mage: {
name: "White Mage",
hp: 18,
atk: 6,
def: 8,
desc: "Heals and supports the party."
},
thief: {
name: "Thief",
hp: 14,
atk: 10,
def: 5,
desc: "Fast and nimble, high critical chance."
},
martial: {
name: "Martial Artist",
hp: 20,
atk: 8,
def: 7,
desc: "Balanced fighter with strong attacks."
},
knight: {
name: "Knight",
hp: 24,
atk: 7,
def: 12,
desc: "High defense, protects the party."
}
};
// Track current HP for each party member during battle
var partyCurrentHP = [];
var statTextObjs = [];
var resetButton = null;
function showCharacterStatsAndReset() {
// Remove old stats if present
for (var i = 0; i < statTextObjs.length; i++) statTextObjs[i].destroy();
statTextObjs = [];
if (resetButton) {
resetButton.destroy();
resetButton = null;
}
var statsY = 2500;
var statsX = 200;
var spacing = 480;
for (var i = 0; i < partyMembers.length; i++) {
var type = partyMembers[i];
var stats = characterStats[type];
var txt = new Text2(stats.name + " HP:" + stats.hp + " ATK:" + stats.atk + " DEF:" + stats.def, {
size: 60,
fill: "#fff"
});
txt.anchor.set(0, 0);
txt.x = statsX + i * spacing;
txt.y = statsY;
LK.gui.bottom.addChild(txt);
statTextObjs.push(txt);
}
// Add reset button below stats
resetButton = new Text2("Reset Game Data", {
size: 70,
fill: 0xFF3366
});
resetButton.anchor.set(0.5, 0);
resetButton.x = 2048 / 2;
resetButton.y = statsY + 120;
resetButton.interactive = true;
resetButton.buttonMode = true;
resetButton._isResetButton = true;
LK.gui.bottom.addChild(resetButton);
}
function clearCharacterStatsAndReset() {
for (var i = 0; i < statTextObjs.length; i++) statTextObjs[i].destroy();
statTextObjs = [];
if (resetButton) {
resetButton.destroy();
resetButton = null;
}
}
// Utility
function resetGame() {
// Reset all state
gameState = STATE_PARTY_SELECT;
selectedParty = [];
collectedOrbs = [false, false, false, false];
for (var i = 0; i < partyIcons.length; i++) partyIcons[i].destroy();
partyIcons = [];
for (var i = 0; i < towerIcons.length; i++) towerIcons[i].destroy();
towerIcons = [];
for (var i = 0; i < orbIcons.length; i++) orbIcons[i].destroy();
orbIcons = [];
if (partySelectText) {
partySelectText.destroy();
partySelectText = null;
}
if (towerSelectText) {
towerSelectText.destroy();
towerSelectText = null;
}
if (heart) {
heart.destroy();
heart = null;
}
for (var i = 0; i < battleBullets.length; i++) battleBullets[i].destroy();
battleBullets = [];
if (battleBossText) {
battleBossText.destroy();
battleBossText = null;
}
if (battleHPText) {
battleHPText.destroy();
battleHPText = null;
}
if (battleOrbsText) {
battleOrbsText.destroy();
battleOrbsText = null;
}
if (battleEndTimeout) {
LK.clearTimeout(battleEndTimeout);
battleEndTimeout = null;
}
for (var i = 0; i < guiParty.length; i++) guiParty[i].destroy();
guiParty = [];
for (var i = 0; i < guiOrbs.length; i++) guiOrbs[i].destroy();
guiOrbs = [];
if (guiHP) {
guiHP.destroy();
guiHP = null;
}
LK.setScore(0);
// Clear persistent stats on full reset
delete storage.selectedParty;
delete storage.collectedOrbs;
startPartySelect();
}
// --- PARTY SELECT ---
function startPartySelect() {
clearCharacterStatsAndReset();
showCharacterStatsAndReset();
gameState = STATE_PARTY_SELECT;
selectedParty = [];
// Show party selection text
partySelectText = new Text2('Choose your party (tap to select 3):', {
size: 90,
fill: '#fff'
});
partySelectText.anchor.set(0.5, 0);
partySelectText.x = 2048 / 2;
partySelectText.y = 180;
game.addChild(partySelectText);
// Show icons for each member
var spacing = 400;
var startX = 2048 / 2 - spacing;
for (var i = 0; i < partyMembers.length; i++) {
var icon = new PartyIcon();
icon.setType(partyMembers[i]);
icon.x = startX + i * spacing;
icon.y = 600;
icon.setSelected(false);
icon.memberId = partyMembers[i];
icon.index = i;
partyIcons.push(icon);
game.addChild(icon);
}
}
// Handle party icon selection
function handlePartySelect(x, y, obj) {
if (gameState !== STATE_PARTY_SELECT) return;
for (var i = 0; i < partyIcons.length; i++) {
var icon = partyIcons[i];
var dx = x - icon.x;
var dy = y - icon.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 80) {
// Toggle selection
var idx = selectedParty.indexOf(icon.memberId);
if (idx === -1 && selectedParty.length < 3) {
selectedParty.push(icon.memberId);
icon.setSelected(true);
} else if (idx !== -1) {
selectedParty.splice(idx, 1);
icon.setSelected(false);
}
// If 3 selected, proceed
if (selectedParty.length === 3) {
// Save party selection persistently
storage.selectedParty = selectedParty.slice();
LK.setTimeout(function () {
for (var j = 0; j < partyIcons.length; j++) partyIcons[j].destroy();
partyIcons = [];
if (partySelectText) {
partySelectText.destroy();
partySelectText = null;
}
startTowerSelect();
}, 400);
}
break;
}
}
}
// --- TOWER SELECT ---
function startTowerSelect() {
clearCharacterStatsAndReset();
showCharacterStatsAndReset();
gameState = STATE_TOWER_SELECT;
// Show tower select text
towerSelectText = new Text2('Choose a tower to conquer:', {
size: 90,
fill: '#fff'
});
towerSelectText.anchor.set(0.5, 0);
towerSelectText.x = 2048 / 2;
towerSelectText.y = 180;
game.addChild(towerSelectText);
// Show towers
var spacing = 350;
var startX = 2048 / 2 - spacing * 2 + spacing / 2;
for (var i = 0; i < towers.length; i++) {
var locked = false;
if (i === 4) {
// Fifth tower locked until all orbs
locked = !(collectedOrbs[0] && collectedOrbs[1] && collectedOrbs[2] && collectedOrbs[3]);
}
var icon = new TowerIcon();
icon.setTower(towers[i].id, locked);
icon.x = startX + i * spacing;
icon.y = 700;
icon.towerIndex = i;
towerIcons.push(icon);
game.addChild(icon);
// Add EX label if in EX mode
if (typeof game !== "undefined" && game._exModeActive) {
var exLabel = new Text2("EX", {
size: 60,
fill: 0xFF3333
});
exLabel.anchor.set(0.5, 0.5);
exLabel.x = icon.x;
exLabel.y = icon.y - 180;
game.addChild(exLabel);
icon._exLabel = exLabel;
}
}
// Show orbs collected
for (var i = 0; i < 4; i++) {
var orb = new OrbIcon();
orb.setOrb(i + 1, collectedOrbs[i]);
orb.x = 2048 / 2 - 200 + i * 130;
orb.y = 1200;
orbIcons.push(orb);
game.addChild(orb);
}
}
// Handle tower icon selection
function handleTowerSelect(x, y, obj) {
if (gameState !== STATE_TOWER_SELECT) return;
for (var i = 0; i < towerIcons.length; i++) {
var icon = towerIcons[i];
var dx = x - icon.x;
var dy = y - (icon.y - 150);
if (dx > -90 && dx < 90 && dy > 0 && dy < 300) {
if (icon.locked) {
// Do nothing
return;
}
// Start battle for this tower
var towerIdx = icon.towerId - 1;
for (var j = 0; j < towerIcons.length; j++) towerIcons[j].destroy();
towerIcons = [];
for (var j = 0; j < orbIcons.length; j++) orbIcons[j].destroy();
orbIcons = [];
if (towerSelectText) {
towerSelectText.destroy();
towerSelectText = null;
}
startBattle(towerIdx);
break;
}
}
}
// --- BATTLE ---
function startBattle(towerIdx) {
clearCharacterStatsAndReset();
gameState = STATE_BATTLE;
battleActive = true;
battleWin = false;
battleLose = false;
battlePhase = 0;
battleTimer = 0;
battleBossId = towerIdx + 1;
if (typeof game !== "undefined" && game._exModeActive) {
// EX mode: more HP
battleBossHP = (40 + towerIdx * 20) * 2.2;
} else {
battleBossHP = 40 + towerIdx * 20;
}
battleBossMaxHP = battleBossHP;
battleBullets = [];
// Initialize each party member's HP
partyCurrentHP = [];
for (var i = 0; i < selectedParty.length; i++) {
var type = selectedParty[i];
partyCurrentHP[i] = characterStats[type].hp;
}
// --- MINOR ENEMY FIGHTS SETUP ---
var minorEnemies = [];
var minorEnemyCount = 2 + towerIdx; // 2,3,4,5,6 for each tower
var minorEnemyHP = 16 + towerIdx * 6;
var minorEnemyIdx = 0;
var inMinorFight = true;
var minorEnemyObj = null;
var minorEnemyText = null;
var minorEnemyHPText = null;
var minorEnemyPatternTimer = 0;
var minorEnemyPatternId = 0;
var minorEnemyNames = ["Goblin", "Slime", "Bat", "Imp", "Wisp", "Rat", "Mimic", "Sprite"];
var minorEnemyClasses = [GoblinEnemy, SlimeEnemy, BatEnemy, ImpEnemy, WispEnemy, RatEnemy, MimicEnemy, SpriteEnemy];
var minorEnemyName = "";
var minorEnemyClass = GoblinEnemy;
// Start with minor enemy fight before boss
inMinorFight = true;
minorEnemyIdx = 0;
// Draw white box for heart movement area
if (typeof heartBox !== "undefined" && heartBox) {
heartBox.destroy();
heartBox = null;
}
var heartBox = new Container();
var boxMinX = 2048 / 2 - 350;
var boxMaxX = 2048 / 2 + 350;
var boxMinY = 1200;
var boxMaxY = 2200;
var boxW = boxMaxX - boxMinX;
var boxH = boxMaxY - boxMinY;
var borderW = 8;
var boxColor = 0xffffff;
// Top border
var topLine = LK.getAsset('bullet', {
width: boxW,
height: borderW,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
topLine.x = boxMinX;
topLine.y = boxMinY;
heartBox.addChild(topLine);
// Bottom border
var bottomLine = LK.getAsset('bullet', {
width: boxW,
height: borderW,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
bottomLine.x = boxMinX;
bottomLine.y = boxMaxY - borderW;
heartBox.addChild(bottomLine);
// Left border
var leftLine = LK.getAsset('bullet', {
width: borderW,
height: boxH,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
leftLine.x = boxMinX;
leftLine.y = boxMinY;
heartBox.addChild(leftLine);
// Right border
var rightLine = LK.getAsset('bullet', {
width: borderW,
height: boxH,
color: boxColor,
shape: 'box',
anchorX: 0,
anchorY: 0
});
rightLine.x = boxMaxX - borderW;
rightLine.y = boxMinY;
heartBox.addChild(rightLine);
game.addChild(heartBox);
// Show heart
heart = new Heart();
heart.x = 2048 / 2;
heart.y = 1800;
heartStartX = heart.x;
heartStartY = heart.y;
game.addChild(heart);
// Show party in GUI
for (var i = 0; i < selectedParty.length; i++) {
var icon = new PartyIcon();
icon.setType(selectedParty[i]);
icon.x = 200 + i * 150;
icon.y = 100;
icon.setSelected(true);
guiParty.push(icon);
LK.gui.top.addChild(icon);
}
// Show orbs in GUI
for (var i = 0; i < 4; i++) {
var orb = new OrbIcon();
orb.setOrb(i + 1, collectedOrbs[i]);
orb.x = 2048 - 200 - (3 - i) * 120;
orb.y = 100;
guiOrbs.push(orb);
LK.gui.top.addChild(orb);
}
// Show HP in GUI
// Show each party member's HP
if (guiHP) {
guiHP.destroy();
guiHP = null;
}
guiHP = [];
for (var i = 0; i < selectedParty.length; i++) {
var hpText = new Text2('❤ ' + partyCurrentHP[i], {
size: 80,
fill: '#ff3366'
});
hpText.anchor.set(0, 0);
hpText.x = 120 + i * 220;
hpText.y = 220;
LK.gui.top.addChild(hpText);
guiHP.push(hpText);
}
// Show orbs
battleOrbsText = new Text2('Orbs: ' + getOrbsCount() + '/4', {
size: 60,
fill: '#fff'
});
battleOrbsText.anchor.set(0.5, 0);
battleOrbsText.x = 2048 / 2;
battleOrbsText.y = 450;
game.addChild(battleOrbsText);
// --- ENEMY ATTACK DELAY STATE ---
var enemyAttackDelayTicks = 180; // 3 seconds at 60fps
var enemyAttackDelayActive = true;
// --- PLAYER TURN MENU REMOVED ---
// (player turn menu, shop, and magic menu functions removed)
var shopMenu = null;
var shopActive = false;
// --- MINOR ENEMY FIGHT LOGIC ---
function startMinorEnemyFight(idx) {
// Clean up previous
if (minorEnemyObj) {
minorEnemyObj.destroy();
minorEnemyObj = null;
}
if (minorEnemyText) {
minorEnemyText.destroy();
minorEnemyText = null;
}
if (minorEnemyHPText) {
minorEnemyHPText.destroy();
minorEnemyHPText = null;
}
minorEnemyPatternTimer = 0;
minorEnemyPatternId = idx % 4;
minorEnemyName = minorEnemyNames[idx % minorEnemyNames.length];
minorEnemyClass = minorEnemyClasses[idx % minorEnemyClasses.length];
// Create enemy
minorEnemyObj = new minorEnemyClass();
minorEnemyObj.hp = minorEnemyHP;
minorEnemyObj.maxHP = minorEnemyHP;
minorEnemyObj.x = 2048 / 2;
minorEnemyObj.y = 700;
game.addChild(minorEnemyObj);
// Name
minorEnemyText = new Text2(minorEnemyName, {
size: 90,
fill: '#fff'
});
minorEnemyText.anchor.set(0.5, 0);
minorEnemyText.x = 2048 / 2;
minorEnemyText.y = 200;
game.addChild(minorEnemyText);
// HP
minorEnemyHPText = new Text2('HP: ' + minorEnemyObj.hp, {
size: 70,
fill: '#fff'
});
minorEnemyHPText.anchor.set(0.5, 0);
minorEnemyHPText.x = 2048 / 2;
minorEnemyHPText.y = 320;
game.addChild(minorEnemyHPText);
// Reset enemy attack delay for each new minor enemy
enemyAttackDelayTicks = 180;
enemyAttackDelayActive = true;
}
function cleanupMinorEnemyFight() {
if (minorEnemyObj) {
minorEnemyObj.destroy();
minorEnemyObj = null;
}
if (minorEnemyText) {
minorEnemyText.destroy();
minorEnemyText = null;
}
if (minorEnemyHPText) {
minorEnemyHPText.destroy();
minorEnemyHPText = null;
}
for (var i = battleBullets.length - 1; i >= 0; i--) {
battleBullets[i].destroy();
battleBullets.splice(i, 1);
}
LK.setScore(0);
}
function updateMinorEnemyFight() {
if (!minorEnemyObj) return;
// Update heart
if (heart) heart.update();
// Update bullets
for (var i = battleBullets.length - 1; i >= 0; i--) {
var b = battleBullets[i];
b.update();
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
// Heart bullet destroys enemy bullet
if (b instanceof HeartBullet) {
// Check collision with enemy bullets
for (var j = battleBullets.length - 1; j >= 0; j--) {
var eb = battleBullets[j];
if (eb === b) continue;
if (eb instanceof EnemyBullet && eb.active && b.active && circleIntersect(b.x, b.y, b.radius, eb.x, eb.y, eb.radius)) {
eb.active = false;
eb.destroy();
b.active = false;
b.destroy();
battleBullets.splice(j, 1);
break;
}
}
// Heart bullet hits minor enemy
if (minorEnemyObj && b.active && circleIntersect(b.x, b.y, b.radius, minorEnemyObj.x, minorEnemyObj.y, 60)) {
minorEnemyObj.hp -= 0.5;
if (minorEnemyObj.hp < 0) minorEnemyObj.hp = 0;
if (minorEnemyHPText) minorEnemyHPText.setText('HP: ' + Math.max(0, Math.round(minorEnemyObj.hp * 10) / 10));
LK.getSound('hit').play();
b.active = false;
b.destroy();
}
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
}
// Collision with heart
if (!heart.invincible && b instanceof EnemyBullet && circleIntersect(heart.x, heart.y, heart.radius, b.x, b.y, b.radius)) {
heart.setInvincible(40);
LK.effects.flashObject(heart, 0xff0000, 400);
LK.getSound('hit').play();
// Randomly select a party member to take damage
if (partyCurrentHP.length > 0) {
var targetIdx = Math.floor(Math.random() * partyCurrentHP.length);
partyCurrentHP[targetIdx] -= 1;
if (partyCurrentHP[targetIdx] < 0) partyCurrentHP[targetIdx] = 0;
if (guiHP && guiHP[targetIdx]) guiHP[targetIdx].setText('❤ ' + partyCurrentHP[targetIdx]);
}
// Lose if all party members are at 0 HP
var allDead = true;
for (var i = 0; i < partyCurrentHP.length; i++) {
if (partyCurrentHP[i] > 0) allDead = false;
}
if (allDead) {
battleLose = true;
battleActive = false;
endBattle(false);
return;
}
}
}
// --- ENEMY ATTACK DELAY LOGIC ---
if (enemyAttackDelayActive) {
enemyAttackDelayTicks--;
if (enemyAttackDelayTicks <= 0) {
enemyAttackDelayActive = false;
}
return; // Don't attack until delay is over
}
// Enemy attack pattern
minorEnemyPatternTimer++;
if (minorEnemyPatternTimer % 40 === 0) {
spawnBulletPattern(minorEnemyPatternId);
// Minor enemy HP drops a bit each pattern
minorEnemyObj.hp--;
if (minorEnemyObj.hp < 0) minorEnemyObj.hp = 0;
if (minorEnemyHPText) minorEnemyHPText.setText('HP: ' + minorEnemyObj.hp);
}
// Win condition: survive or reduce enemy HP
if (minorEnemyObj.hp <= 0) {
// Next enemy or boss
cleanupMinorEnemyFight();
minorEnemyIdx++;
if (minorEnemyIdx < minorEnemyCount) {
startMinorEnemyFight(minorEnemyIdx);
} else {
// Proceed to boss
inMinorFight = false;
startBossFight();
}
}
}
function startBossFight() {
// Clean up minor enemy UI
if (minorEnemyObj) {
minorEnemyObj.destroy();
minorEnemyObj = null;
}
if (minorEnemyText) {
minorEnemyText.destroy();
minorEnemyText = null;
}
if (minorEnemyHPText) {
minorEnemyHPText.destroy();
minorEnemyHPText = null;
}
for (var i = battleBullets.length - 1; i >= 0; i--) {
battleBullets[i].destroy();
battleBullets.splice(i, 1);
}
LK.setScore(0);
// Show boss name and image
var bossNames = ['Amethyst Guardian', 'Emerald Warden', 'Amber Sentinel', 'Crimson Overlord', 'Obsidian Nemesis'];
var bossClasses = [AmethystBoss, EmeraldBoss, AmberBoss, CrimsonBoss, ObsidianBoss];
battleBossText = new Text2(bossNames[towerIdx], {
size: 100,
fill: '#fff'
});
battleBossText.anchor.set(0.5, 0);
battleBossText.x = 2048 / 2;
battleBossText.y = 200;
game.addChild(battleBossText);
// Show boss image
if (typeof battleBossSprite !== "undefined" && battleBossSprite) {
battleBossSprite.destroy();
battleBossSprite = null;
}
battleBossSprite = new bossClasses[towerIdx]();
battleBossSprite.x = 2048 / 2;
battleBossSprite.y = 500;
game.addChild(battleBossSprite);
// Show HP
battleHPText = new Text2('HP: ' + battleBossHP, {
size: 80,
fill: '#fff'
});
battleHPText.anchor.set(0.5, 0);
battleHPText.x = 2048 / 2;
battleHPText.y = 350;
game.addChild(battleHPText);
LK.setScore(0);
battleTimer = 0;
battlePhase = 0;
battleActive = true;
// Sword event variables
battleSwordEvent = null;
battleSwordEventTimer = 0;
// Reset enemy attack delay for boss
enemyAttackDelayTicks = 180;
enemyAttackDelayActive = true;
}
// Start music
if (towerIdx === 4) {
LK.playMusic('boss_theme');
} else {
LK.playMusic('battle_theme');
}
// Start first minor enemy
startMinorEnemyFight(0);
// Patch updateBattle to handle minor enemies before boss
var _originalUpdateBattle = updateBattle;
updateBattle = function updateBattle() {
if (gameState !== STATE_BATTLE || !battleActive) return;
if (inMinorFight) {
updateMinorEnemyFight();
return;
}
// Boss fight as before
// (player turn menu logic removed)
// Update heart
if (heart) heart.update();
// Update bullets
for (var i = battleBullets.length - 1; i >= 0; i--) {
var b = battleBullets[i];
b.update();
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
// Collision with heart
if (!heart.invincible && circleIntersect(heart.x, heart.y, heart.radius, b.x, b.y, b.radius)) {
heart.setInvincible(40);
LK.effects.flashObject(heart, 0xff0000, 400);
LK.getSound('hit').play();
// Randomly select a party member to take damage
if (partyCurrentHP.length > 0) {
var targetIdx = Math.floor(Math.random() * partyCurrentHP.length);
partyCurrentHP[targetIdx] -= 1;
if (partyCurrentHP[targetIdx] < 0) partyCurrentHP[targetIdx] = 0;
if (guiHP && guiHP[targetIdx]) guiHP[targetIdx].setText('❤ ' + partyCurrentHP[targetIdx]);
}
// Lose if all party members are at 0 HP
var allDead = true;
for (var i = 0; i < partyCurrentHP.length; i++) {
if (partyCurrentHP[i] > 0) allDead = false;
}
if (allDead) {
battleLose = true;
battleActive = false;
endBattle(false);
return;
}
}
}
// --- ENEMY ATTACK DELAY LOGIC ---
if (enemyAttackDelayActive) {
enemyAttackDelayTicks--;
if (enemyAttackDelayTicks <= 0) {
enemyAttackDelayActive = false;
}
return; // Don't attack until delay is over
}
// Boss attack pattern
battleTimer++;
if (battleBossId === 5) {
// Final boss: mix of all patterns
if (battleTimer % 30 === 0) {
spawnBulletPattern(battleTimer / 300 % 4 | 0);
}
} else {
if (battleTimer % 40 === 0) {
spawnBulletPattern(battleBossId - 1);
}
}
// --- Sword Event Logic ---
if (typeof battleSwordEventTimer === "undefined") battleSwordEventTimer = 0;
if (typeof battleSwordEvent === "undefined") battleSwordEvent = null;
// Only spawn sword event if boss fight is active
if (battleActive) {
// If no sword event, randomly spawn one every 180-300 ticks
if (!battleSwordEvent || !battleSwordEvent.active) {
battleSwordEventTimer++;
// Randomly spawn between 180 and 300 ticks
if (battleSwordEventTimer > 180 + Math.floor(Math.random() * 120)) {
battleSwordEventTimer = 0;
battleSwordEvent = new SwordEvent();
// Place sword randomly in heart box area, but always on the same row as the heart
var minX = 2048 / 2 - 300,
maxX = 2048 / 2 + 300;
battleSwordEvent.x = minX + Math.random() * (maxX - minX);
// Place sword at the same y as the heart (heart.y), or default to 1800 if heart is missing
battleSwordEvent.y = heart && typeof heart.y === "number" ? heart.y : 1800;
game.addChild(battleSwordEvent);
}
}
// Update sword event if present
if (battleSwordEvent && battleSwordEvent.active) {
battleSwordEvent.update();
// If heart touches sword, deal damage to boss and destroy sword
if (heart && !heart.invincible && circleIntersect(heart.x, heart.y, heart.radius, battleSwordEvent.x, battleSwordEvent.y, battleSwordEvent.radius)) {
battleBossHP -= 5;
if (battleBossHP < 0) battleBossHP = 0;
if (battleHPText) battleHPText.setText('HP: ' + Math.round(battleBossHP * 10) / 10);
LK.getSound('hit').play();
battleSwordEvent.active = false;
battleSwordEvent.destroy();
}
}
}
// Win condition: survive for N ticks or reduce boss HP
if (battleBossHP <= 0) {
battleWin = true;
battleActive = false;
endBattle(true);
return;
}
};
}
// Get orbs count
function getOrbsCount() {
var c = 0;
for (var i = 0; i < collectedOrbs.length; i++) if (collectedOrbs[i]) c++;
return c;
}
// Handle heart drag
function handleHeartDrag(x, y, obj) {
if (gameState !== STATE_BATTLE || !battleActive) return;
if (!heart) return;
// Clamp to battle box
var minX = 2048 / 2 - 350,
maxX = 2048 / 2 + 350;
var minY = 1200,
maxY = 2200;
var prevX = heart.x;
var prevY = heart.y;
heart.x = Math.max(minX, Math.min(maxX, x));
heart.y = Math.max(minY, Math.min(maxY, y));
// Shoot yellow bullet if moved (dragged)
if (typeof heart.lastShootTick === "undefined") heart.lastShootTick = 0;
if (typeof heartShootCooldown === "undefined") heartShootCooldown = 4;
if (Math.abs(heart.x - prevX) > 2 || Math.abs(heart.y - prevY) > 2) {
// Only shoot if enough ticks have passed since last shot
if (typeof LK !== "undefined" && LK.ticks - heart.lastShootTick > heartShootCooldown) {
var b = new HeartBullet();
b.init(heart.x, heart.y - heart.radius - 10);
if (typeof battleBullets !== "undefined") {
battleBullets.push(b);
}
if (typeof game !== "undefined") {
game.addChild(b);
}
heart.lastShootTick = LK.ticks;
}
}
}
// Handle heart down
function handleHeartDown(x, y, obj) {
if (gameState !== STATE_BATTLE || !battleActive) return;
if (!heart) return;
var dx = x - heart.x;
var dy = y - heart.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < heart.radius + 10) {
heartDragging = true;
heartDragOffsetX = dx;
heartDragOffsetY = dy;
}
}
// Handle heart up
function handleHeartUp(x, y, obj) {
if (gameState !== STATE_BATTLE || !battleActive) return;
heartDragging = false;
}
// --- BATTLE UPDATE ---
function updateBattle() {
if (gameState !== STATE_BATTLE || !battleActive) return;
// Update heart
if (heart) heart.update();
// Update bullets
for (var i = battleBullets.length - 1; i >= 0; i--) {
var b = battleBullets[i];
b.update();
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
// Heart bullet destroys enemy bullet
if (b instanceof HeartBullet) {
// Check collision with enemy bullets
for (var j = battleBullets.length - 1; j >= 0; j--) {
var eb = battleBullets[j];
if (eb === b) continue;
// Only destroy enemy bullets (not other heart bullets)
if (eb instanceof EnemyBullet && eb.active && b.active && circleIntersect(b.x, b.y, b.radius, eb.x, eb.y, eb.radius)) {
if (typeof eb.hp === "number" && eb.hp > 1) {
eb.hp--;
b.active = false;
b.destroy();
// Play hit sound for feedback
LK.getSound('hit').play();
// If hp drops to 0, destroy
if (eb.hp <= 0) {
eb.active = false;
eb.destroy();
battleBullets.splice(j, 1);
}
} else {
eb.active = false;
eb.destroy();
b.active = false;
b.destroy();
// Remove both from array
battleBullets.splice(j, 1);
}
break;
}
}
// Heart bullet hits boss
if (battleBossSprite && b.active && circleIntersect(b.x, b.y, b.radius, battleBossSprite.x, battleBossSprite.y, 90)) {
battleBossHP -= 0.5;
if (battleBossHP < 0) battleBossHP = 0;
if (battleHPText) battleHPText.setText('HP: ' + Math.round(battleBossHP * 10) / 10);
LK.getSound('hit').play();
b.active = false;
b.destroy();
}
if (!b.active) {
battleBullets.splice(i, 1);
continue;
}
}
// Collision with heart
if (!heart.invincible && b instanceof EnemyBullet && circleIntersect(heart.x, heart.y, heart.radius, b.x, b.y, b.radius)) {
// Hit!
heart.setInvincible(40);
LK.effects.flashObject(heart, 0xff0000, 400);
LK.getSound('hit').play();
LK.setScore(LK.getScore() + 1);
// Lose if hit 3 times
if (LK.getScore() >= 3) {
battleLose = true;
battleActive = false;
endBattle(false);
return;
}
}
}
// Boss attack pattern
battleTimer++;
if (battleBossId === 5) {
// Final boss: mix of all patterns
if (battleTimer % 30 === 0) {
spawnBulletPattern(battleTimer / 300 % 4 | 0);
}
} else {
if (battleTimer % 40 === 0) {
spawnBulletPattern(battleBossId - 1);
}
}
// Win condition: survive for N ticks or reduce boss HP
if (battleBossHP <= 0) {
battleWin = true;
battleActive = false;
endBattle(true);
return;
}
}
// --- BATTLE PATTERNS ---
function spawnBulletPattern(patternId) {
var cx = 2048 / 2,
cy = 1700;
// Determine EX mode bullet speed multiplier
var exSpeed = typeof game !== "undefined" && game._exModeActive ? 1.6 : 1;
if (patternId === 0) {
// Simple downward rain
for (var i = 0; i < 5; i++) {
var b = new EnemyBullet();
b.init(cx - 300 + 150 * i, 1200, 0, 12 * exSpeed, true); // true = boss fight bullet
battleBullets.push(b);
game.addChild(b);
}
} else if (patternId === 1) {
// Sine wave
for (var i = 0; i < 4; i++) {
var b = new EnemyBullet();
b.init(cx - 200 + 200 * i, 1200, Math.sin(LK.ticks / 20 + i) * 6 * exSpeed, 10 * exSpeed, true);
battleBullets.push(b);
game.addChild(b);
}
} else if (patternId === 2) {
// Spiral
var angle = LK.ticks % 360 / 180 * Math.PI;
for (var i = 0; i < 6; i++) {
var a = angle + i * Math.PI / 3;
var b = new EnemyBullet();
b.init(cx, 1400, Math.cos(a) * 8 * exSpeed, (Math.sin(a) * 8 + 8) * exSpeed, true);
battleBullets.push(b);
game.addChild(b);
}
} else if (patternId === 3) {
// Random bursts
for (var i = 0; i < 8; i++) {
var a = Math.random() * Math.PI * 2;
var b = new EnemyBullet();
b.init(cx, 1500, Math.cos(a) * 10 * exSpeed, (Math.sin(a) * 10 + 10) * exSpeed, true);
battleBullets.push(b);
game.addChild(b);
}
}
// Boss HP drops a bit each pattern
battleBossHP--;
if (battleBossHP < 0) battleBossHP = 0;
if (battleHPText) battleHPText.setText('HP: ' + Math.round(battleBossHP * 10) / 10);
}
// --- END BATTLE ---
function endBattle(win) {
if (win) {
LK.getSound('boss_defeat').play();
// Collect orb if not final boss
if (battleBossId <= 4) {
collectedOrbs[battleBossId - 1] = true;
// Save orbs persistently
storage.collectedOrbs = collectedOrbs.slice();
LK.getSound('collect_orb').play();
}
// Show win text
var winText = new Text2('Victory!', {
size: 140,
fill: '#fff'
});
winText.anchor.set(0.5, 0.5);
winText.x = 2048 / 2;
winText.y = 1200;
game.addChild(winText);
LK.effects.flashScreen(0x00ff00, 800);
battleEndTimeout = LK.setTimeout(function () {
winText.destroy();
cleanupBattle();
if (battleBossId === 5) {
// Game complete
showGameComplete();
} else {
startTowerSelect();
}
}, 1800);
} else {
// Lose
var loseText = new Text2('Defeated!', {
size: 140,
fill: '#fff'
});
loseText.anchor.set(0.5, 0.5);
loseText.x = 2048 / 2;
loseText.y = 1200;
game.addChild(loseText);
LK.effects.flashScreen(0xff0000, 800);
battleEndTimeout = LK.setTimeout(function () {
loseText.destroy();
cleanupBattle();
startTowerSelect();
}, 1800);
}
}
// --- CLEANUP BATTLE ---
function cleanupBattle() {
if (heart) {
heart.destroy();
heart = null;
}
// Remove heartBox if present
if (typeof heartBox !== "undefined" && heartBox) {
heartBox.destroy();
heartBox = null;
}
for (var i = 0; i < battleBullets.length; i++) battleBullets[i].destroy();
battleBullets = [];
if (battleBossText) {
battleBossText.destroy();
battleBossText = null;
}
// Remove sword event if present
if (typeof battleSwordEvent !== "undefined" && battleSwordEvent) {
battleSwordEvent.destroy();
battleSwordEvent = null;
}
// Remove boss sprite if present
if (typeof battleBossSprite !== "undefined" && battleBossSprite) {
battleBossSprite.destroy();
battleBossSprite = null;
}
if (battleHPText) {
battleHPText.destroy();
battleHPText = null;
}
if (battleOrbsText) {
battleOrbsText.destroy();
battleOrbsText = null;
}
for (var i = 0; i < guiParty.length; i++) guiParty[i].destroy();
guiParty = [];
for (var i = 0; i < guiOrbs.length; i++) guiOrbs[i].destroy();
guiOrbs = [];
if (guiHP) {
if (Array.isArray(guiHP)) {
for (var i = 0; i < guiHP.length; i++) {
if (guiHP[i]) guiHP[i].destroy();
}
} else {
guiHP.destroy();
}
guiHP = null;
}
LK.setScore(0);
LK.stopMusic();
}
// --- GAME COMPLETE ---
function showGameComplete() {
clearCharacterStatsAndReset();
gameState = STATE_GAME_COMPLETE;
var completeText = new Text2('You conquered all towers!\nQuest Complete!', {
size: 120,
fill: '#fff'
});
completeText.anchor.set(0.5, 0.5);
completeText.x = 2048 / 2;
completeText.y = 1200;
game.addChild(completeText);
LK.effects.flashScreen(0x00ffcc, 1200);
LK.setTimeout(function () {
completeText.destroy();
// Offer EX Tower mode if not already in EX mode
if (!game._exModeActive) {
// Show EX mode prompt
var exText = new Text2('Secret EX Towers unlocked!\nTap to challenge EX Towers!', {
size: 100,
fill: '#ff3333'
});
exText.anchor.set(0.5, 0.5);
exText.x = 2048 / 2;
exText.y = 1200;
game.addChild(exText);
// Set up tap to start EX mode
var _exModeHandler = function exModeHandler(x, y, obj) {
if (exText && exText.parent) {
var bx = exText.x - exText.width * exText.anchor.x;
var by = exText.y - exText.height * exText.anchor.y;
var bw = exText.width;
var bh = exText.height;
if (x >= bx && x <= bx + bw && y >= by && y <= by + bh) {
exText.destroy();
game.off('down', _exModeHandler);
startEXMode();
}
}
};
game.on('down', _exModeHandler);
} else {
// If already in EX mode, reset to normal game
resetGame();
}
}, 3000);
}
// --- EX TOWER MODE ---
function startEXMode() {
// Mark EX mode active
game._exModeActive = true;
// Reset all state, but mark EX mode
gameState = STATE_TOWER_SELECT;
selectedParty = [];
// Keep orbs and party, but reset towers
for (var i = 0; i < partyIcons.length; i++) partyIcons[i].destroy();
partyIcons = [];
for (var i = 0; i < towerIcons.length; i++) towerIcons[i].destroy();
towerIcons = [];
for (var i = 0; i < orbIcons.length; i++) orbIcons[i].destroy();
orbIcons = [];
if (partySelectText) {
partySelectText.destroy();
partySelectText = null;
}
if (towerSelectText) {
towerSelectText.destroy();
towerSelectText = null;
}
if (heart) {
heart.destroy();
heart = null;
}
for (var i = 0; i < battleBullets.length; i++) battleBullets[i].destroy();
battleBullets = [];
if (battleBossText) {
battleBossText.destroy();
battleBossText = null;
}
if (battleHPText) {
battleHPText.destroy();
battleHPText = null;
}
if (battleOrbsText) {
battleOrbsText.destroy();
battleOrbsText = null;
}
if (battleEndTimeout) {
LK.clearTimeout(battleEndTimeout);
battleEndTimeout = null;
}
for (var i = 0; i < guiParty.length; i++) guiParty[i].destroy();
guiParty = [];
for (var i = 0; i < guiOrbs.length; i++) guiOrbs[i].destroy();
guiOrbs = [];
if (guiHP) {
if (Array.isArray(guiHP)) {
for (var i = 0; i < guiHP.length; i++) {
if (guiHP[i]) guiHP[i].destroy();
}
} else {
guiHP.destroy();
}
guiHP = null;
}
LK.setScore(0);
// Show EX tower select
startTowerSelect();
}
// --- CIRCLE INTERSECT ---
function circleIntersect(x1, y1, r1, x2, y2, r2) {
var dx = x1 - x2,
dy = y1 - y2;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < r1 + r2 - 10;
}
// --- GAME EVENTS ---
game.down = function (x, y, obj) {
// Check for reset button tap
if (resetButton && resetButton.parent) {
var btn = resetButton;
var bx = btn.x - btn.width * btn.anchor.x;
var by = btn.y - btn.height * btn.anchor.y;
var bw = btn.width;
var bh = btn.height;
// Convert to LK.gui.bottom coordinates
var local = LK.gui.bottom.toLocal({
x: x,
y: y
});
if (local.x >= bx && local.x <= bx + bw && local.y >= by && local.y <= by + bh) {
resetGame();
return;
}
}
// (player turn menu and shop/magic menu input handling removed)
if (gameState === STATE_PARTY_SELECT) {
handlePartySelect(x, y, obj);
} else if (gameState === STATE_TOWER_SELECT) {
handleTowerSelect(x, y, obj);
} else if (gameState === STATE_BATTLE && battleActive) {
handleHeartDown(x, y, obj);
}
};
game.move = function (x, y, obj) {
if (gameState === STATE_BATTLE && battleActive && heartDragging) {
handleHeartDrag(x - heartDragOffsetX, y - heartDragOffsetY, obj);
}
};
game.up = function (x, y, obj) {
if (gameState === STATE_BATTLE && battleActive) {
handleHeartUp(x, y, obj);
}
};
// --- GAME UPDATE ---
game.update = function () {
if (gameState === STATE_BATTLE && battleActive) {
updateBattle();
}
};
// --- START GAME ---
LK.playMusic('main_theme');
resetGame();
// Show username at the bottom of the screen
if (typeof usernameText !== "undefined" && usernameText) {
usernameText.destroy();
usernameText = null;
}
var username = storage.username || "Player";
var usernameText = new Text2(username, {
size: 60,
fill: "#fff"
});
usernameText.anchor.set(0.5, 1);
usernameText.x = 2048 / 2;
usernameText.y = 2732 - 20;
LK.gui.bottom.addChild(usernameText);
8-bit heart. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
martial artist. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
8- bit girl thief. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
White mage 8-bit girl. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A diamond shaped white bullet. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
purple orb. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
green emerald. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
orange diamond . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
red crystal. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
make a 8-bit knight. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
8-bit RPG tower. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Green Tower. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A tree with a door on top of a hill. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A castle surrounded by red hot lava. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a twisted castle with creepy trees on the ground. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a realistic whisp of blue fire. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a goblin with a dagger. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
realistic slime monster. In-Game asset. 2d. High contrast. No shadows
a realistic rat. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a red chest mimic. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Realistic Imp. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a pile of money that is also a mimic and realistic. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a brick mimic realistic. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a goblin with a dagger realistic. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a realistic bat. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a white bullet. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a giant realistic monster made out of obsidian. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a king made out of emerald. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a red ruby monster with a two handed greatsword. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
amethyst knight. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
final boss holding a white mage from the game and the knight from the game. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a sword on a yellow button. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat