/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Card class
var Card = Container.expand(function () {
var self = Container.call(this);
// Card data: {type, name, desc, damage, block, special}
self.cardData = null;
// Card background
var cardBg = null;
// Card text
var nameTxt = null;
var descTxt = null;
// Set card data and visuals
self.setCard = function (cardData) {
self.cardData = cardData;
if (cardBg) cardBg.destroy();
if (nameTxt) nameTxt.destroy();
if (descTxt) descTxt.destroy();
if (self.staminaTxt) {
self.staminaTxt.destroy();
self.staminaTxt = null;
}
var assetId = 'card';
if (cardData.type === 'attack') assetId = 'cardAttack';else if (cardData.type === 'defend') assetId = 'cardDefend';else if (cardData.type === 'special') assetId = 'cardSpecial';
cardBg = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
nameTxt = new Text2(cardData.name, {
size: 48,
fill: '#ffffff'
});
nameTxt.anchor.set(0.5, 0);
nameTxt.x = 0;
nameTxt.y = cardBg.height / 2 + 10;
self.addChild(nameTxt);
// Card description text (explains what the card does, e.g. "Deal 6 damage", "Gain 5 block")
// This text is shown under the card image, centered, and uses white color for clarity
descTxt = new Text2(cardData.desc, {
size: 32,
fill: '#ffffff'
});
descTxt.anchor.set(0.5, 0);
descTxt.x = 0;
descTxt.y = cardBg.height / 2 + 70;
self.addChild(descTxt);
// Show stamina cost
var cost = cardData.stamina || 1;
self.staminaTxt = new Text2(cost + '', {
size: 48,
fill: '#ffaa00'
});
self.staminaTxt.anchor.set(0.5, 0.5);
self.staminaTxt.x = 0;
self.staminaTxt.y = 160;
self.addChild(self.staminaTxt);
};
// Highlight for selection
self.setSelected = function (selected) {
if (cardBg) {
cardBg.alpha = selected ? 1 : 0.85;
cardBg.scaleX = cardBg.scaleY = selected ? 1.08 : 1;
}
};
// For drag/press
self.down = function (x, y, obj) {
if (self.onCardDown) self.onCardDown(self, x, y, obj);
};
return self;
});
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
self.maxHp = 1;
self.hp = 1;
self.intent = null; // {type, value}
self.enemyData = null;
var enemyShape = null;
var hpTxt = new Text2('', {
size: 48,
fill: '#ffffff'
});
hpTxt.anchor.set(0.5, 0.5);
hpTxt.x = 0;
hpTxt.y = 0;
self.addChild(hpTxt);
var intentTxt = new Text2('', {
size: 36,
fill: '#ffcc00'
});
intentTxt.anchor.set(0.5, 0);
intentTxt.x = 0;
intentTxt.y = 120;
self.addChild(intentTxt);
self.setEnemy = function (enemyData) {
self.enemyData = enemyData;
self.maxHp = enemyData.hp;
self.hp = enemyData.hp;
// Remove old asset if present
if (enemyShape) enemyShape.destroy();
// Use boss asset for boss enemy, otherwise normal enemy asset
var assetId = enemyData.name === 'Boss' ? 'boss' : 'enemy';
enemyShape = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.addChildAt(enemyShape, 0);
self.updateHp();
self.setIntent(enemyData.intent);
};
self.setIntent = function (intent) {
self.intent = intent;
if (intent && intent.type === 'attack') {
intentTxt.setText('Attack\n' + intent.value);
} else if (intent && intent.type === 'block') {
intentTxt.setText('Block\n' + intent.value);
} else {
intentTxt.setText('');
}
};
self.updateHp = function () {
hpTxt.setText(self.hp + ' / ' + self.maxHp);
};
return self;
});
// MapNode class
var MapNode = Container.expand(function () {
var self = Container.call(this);
self.nodeType = 'normal'; // 'normal', 'boss'
self.nodeIndex = 0;
self.isCurrent = false;
self.isVisited = false;
var nodeShape = null;
var nodeTxt = null;
self.setNode = function (type, isCurrent, isBoss) {
self.nodeType = type;
self.isCurrent = isCurrent;
if (nodeShape) nodeShape.destroy();
if (nodeTxt) nodeTxt.destroy();
var assetId = isBoss ? 'mapNodeBoss' : isCurrent ? 'mapNodeCurrent' : 'mapNode';
nodeShape = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
nodeTxt = new Text2(isBoss ? 'B' : '', {
size: 64,
fill: '#222222'
});
nodeTxt.anchor.set(0.5, 0.5);
nodeTxt.x = 0;
nodeTxt.y = 0;
self.addChild(nodeTxt);
};
// For tap
self.down = function (x, y, obj) {
if (self.onNodeDown) self.onNodeDown(self, x, y, obj);
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
self.maxHp = 30;
self.hp = 30;
self.block = 0;
var playerShape = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.4,
scaleY: 1.4
});
// Health text under player
var hpTxt = new Text2('', {
size: 48,
fill: '#ffffff'
});
hpTxt.anchor.set(0.5, 0);
hpTxt.x = 0;
hpTxt.y = 140; // Place under the player sprite
self.addChild(hpTxt);
var blockTxt = new Text2('', {
size: 36,
fill: '#55aaff'
});
blockTxt.anchor.set(0.5, 0);
blockTxt.x = 0;
blockTxt.y = 200; // Place block text further below health
self.addChild(blockTxt);
self.updateStats = function () {
hpTxt.setText(self.hp + ' / ' + self.maxHp);
blockTxt.setText('Block: ' + self.block);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// --- Card Data ---
// Card shapes (rectangles for cards)
var CARD_LIBRARY = [{
type: 'attack',
name: 'Strike',
desc: 'Deal 6 damage.',
damage: 6,
stamina: 1
}, {
type: 'defend',
name: 'Defend',
desc: 'Gain 5 block.',
block: 5,
stamina: 1
}, {
type: 'attack',
name: 'Slash',
desc: 'Deal 8 damage.',
damage: 8,
stamina: 2
}, {
type: 'defend',
name: 'Guard',
desc: 'Gain 7 block.',
block: 7,
stamina: 2
}, {
type: 'special',
name: 'Heal',
desc: 'Heal 4 HP.',
special: 'heal',
value: 4,
stamina: 2
}, {
type: 'special',
name: 'Double Strike',
desc: 'Deal 4 damage twice.',
special: 'double',
value: 4,
stamina: 2
}, {
type: 'special',
name: 'Bash',
desc: 'Deal 10 damage. Next attack +3.',
special: 'bash',
value: 10,
stamina: 2
}];
// --- Enemy Data ---
var ENEMY_LIBRARY = [{
name: 'Goblin',
hp: 18,
intent: {
type: 'attack',
value: 5
}
}, {
name: 'Slime',
hp: 22,
intent: {
type: 'block',
value: 6
}
}, {
name: 'Orc',
hp: 28,
intent: {
type: 'attack',
value: 8
}
}, {
name: 'Boss',
hp: 40,
intent: {
type: 'attack',
value: 12
}
}];
// --- Game State ---
var player = null;
var enemy = null;
var hand = [];
var deck = [];
var discard = [];
var drawPile = [];
var selectedCard = null;
var cardNodes = [];
var enemyNode = null;
var playerNode = null;
var mapNodes = [];
var mapPaths = [];
var currentMapIndex = 0;
var mapData = [];
var inBattle = false;
var battleResult = null; // null, 'win', 'lose'
var rewardCards = [];
var rewardCardNodes = [];
var rewardTxt = null;
var dragCard = null;
var dragStart = null;
var dragValid = false;
var guiHpTxt = null;
var guiEnemyHpTxt = null;
var guiFloorTxt = null;
var guiBlockTxt = null;
var guiEnemyIntentTxt = null;
var guiMapGroup = null;
var guiHandGroup = null;
var guiBattleGroup = null;
var guiRewardGroup = null;
var guiStaminaTxt = null;
var floorNum = 1;
var maxFloors = 8;
// Stamina system
var playerStamina = 3;
var playerMaxStamina = 3;
// --- Utility Functions ---
function shuffleArray(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
function cloneCard(card) {
var c = {};
for (var k in card) c[k] = card[k];
return c;
}
function getRandomCard() {
var idx = Math.floor(Math.random() * CARD_LIBRARY.length);
return cloneCard(CARD_LIBRARY[idx]);
}
function getRandomEnemy(isBoss) {
if (isBoss) return ENEMY_LIBRARY[3];
var idx = Math.floor(Math.random() * 3);
return ENEMY_LIBRARY[idx];
}
// --- Map Generation ---
function generateMap() {
// Simple linear with 2-branch at each node, boss at end
mapData = [];
for (var i = 0; i < maxFloors - 1; ++i) {
var node = {
type: 'normal',
isBoss: false,
index: i
};
mapData.push(node);
}
mapData.push({
type: 'boss',
isBoss: true,
index: maxFloors - 1
});
}
// --- Map Rendering ---
function renderMap() {
// Add map background if not present
if (!game.mapBg) {
game.mapBg = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
game.addChildAt(game.mapBg, 0);
}
game.mapBg.visible = true;
// Add boss background if not present
if (!game.bossBg) {
game.bossBg = LK.getAsset('Background', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
game.addChildAt(game.bossBg, 0);
}
// Show boss background only if on boss node, otherwise hide
if (mapData && mapData[currentMapIndex] && mapData[currentMapIndex].isBoss) {
game.bossBg.visible = true;
game.mapBg.visible = false;
} else {
game.bossBg.visible = false;
game.mapBg.visible = true;
}
if (!guiMapGroup) {
guiMapGroup = new Container();
game.addChild(guiMapGroup);
}
// Clear old
for (var i = 0; i < mapNodes.length; ++i) mapNodes[i].destroy();
mapNodes = [];
for (var i = 0; i < mapPaths.length; ++i) mapPaths[i].destroy();
mapPaths = [];
var startY = 350;
var gapY = 220;
var centerX = 2048 / 2;
for (var i = 0; i < mapData.length; ++i) {
var node = new MapNode();
var isCurrent = i === currentMapIndex;
node.setNode(mapData[i].type, isCurrent, mapData[i].isBoss);
node.x = centerX;
node.y = startY + i * gapY;
node.nodeIndex = i;
node.onNodeDown = onMapNodeDown;
guiMapGroup.addChild(node);
mapNodes.push(node);
// Path to next
if (i < mapData.length - 1) {
var path = LK.getAsset('mapPath', {
anchorX: 0.5,
anchorY: 0,
x: centerX,
y: node.y + 60,
scaleY: 1.2
});
guiMapGroup.addChild(path);
mapPaths.push(path);
}
}
}
// --- Map Node Click ---
function onMapNodeDown(node, x, y, obj) {
if (inBattle) return;
if (node.nodeIndex === currentMapIndex) {
// Enter battle
startBattle(node.nodeIndex);
}
}
// --- Start Battle ---
function startBattle(mapIdx) {
inBattle = true;
// Remove map
if (guiMapGroup) guiMapGroup.visible = false;
// Hide map and boss backgrounds, show cave background
if (game.mapBg) game.mapBg.visible = false;
if (game.bossBg) game.bossBg.visible = false;
// Setup cave background
if (!game.caveBg) {
game.caveBg = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
game.addChildAt(game.caveBg, 0);
} else {
game.caveBg.visible = true;
}
// Setup player sprite on left
if (!playerNode) {
playerNode = new Player();
playerNode.x = 400;
playerNode.y = 1700;
game.addChild(playerNode);
} else {
playerNode.x = 400;
playerNode.y = 1700;
playerNode.visible = true;
}
playerNode.updateStats();
// Add stamina text above player sprite
if (!playerNode.staminaTxt) {
playerNode.staminaTxt = new Text2('', {
size: 44,
fill: '#ffaa00'
});
playerNode.staminaTxt.anchor.set(0.5, 0.5);
playerNode.staminaTxt.x = 0;
playerNode.staminaTxt.y = -160;
playerNode.addChild(playerNode.staminaTxt);
}
playerNode.staminaTxt.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
playerNode.staminaTxt.visible = true;
// Setup enemy/enemies on right
if (enemyNode) enemyNode.destroy();
enemyNode = new Enemy();
enemyNode.setEnemy(getRandomEnemy(mapData[mapIdx].isBoss));
// Set enemy height to match player height
var playerHeight = playerNode.height;
var enemyHeight = enemyNode.height;
if (enemyHeight !== 0) {
var scale = playerHeight / enemyHeight;
enemyNode.scaleX = scale;
enemyNode.scaleY = scale;
}
// Align enemy horizontally with player (same y)
enemyNode.x = 1648;
enemyNode.y = playerNode.y;
game.addChild(enemyNode);
// For future: support multiple enemies
// (for now, just one enemyNode, but can be extended to an array of Enemy nodes)
// Setup deck/hand
if (deck.length === 0) {
// Starting deck: 5x Strike, 5x Defend
deck = [];
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[0]));
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[1]));
}
drawPile = [];
for (var i = 0; i < deck.length; ++i) drawPile.push(cloneCard(deck[i]));
shuffleArray(drawPile);
discard = [];
hand = [];
drawHand();
// Show battle group
if (!guiBattleGroup) {
guiBattleGroup = new Container();
game.addChild(guiBattleGroup);
}
guiBattleGroup.visible = true;
// Hide reward group if present
if (guiRewardGroup) guiRewardGroup.visible = false;
// Floor text
if (!guiFloorTxt) {
guiFloorTxt = new Text2('', {
size: 64,
fill: '#ffffff'
});
guiFloorTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(guiFloorTxt);
}
guiFloorTxt.setText('Floor ' + (mapIdx + 1));
// Reset player stats
playerNode.block = 0;
playerNode.updateStats();
// Reset stamina
playerMaxStamina = 3 + Math.floor(currentMapIndex / 2); // Optionally scale with progress
playerStamina = playerMaxStamina;
// Show stamina UI as a bar and text above cards, visually distinct
if (!guiStaminaTxt) {
// Stamina bar background
guiStaminaTxt = new Container();
guiStaminaTxt.bg = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 0.25,
x: 0,
y: 0
});
guiStaminaTxt.bg.alpha = 0.18;
guiStaminaTxt.addChild(guiStaminaTxt.bg);
// Stamina bar fill
guiStaminaTxt.bar = LK.getAsset('cardAttack', {
anchorX: 0,
anchorY: 0.5,
scaleX: 2.1,
scaleY: 0.18,
x: -160,
y: 0
});
guiStaminaTxt.bar.alpha = 0.45;
guiStaminaTxt.addChild(guiStaminaTxt.bar);
// Stamina text
guiStaminaTxt.text = new Text2('', {
size: 54,
fill: '#ffaa00'
});
guiStaminaTxt.text.anchor.set(0.5, 0.5);
guiStaminaTxt.text.x = 0;
guiStaminaTxt.text.y = 0;
guiStaminaTxt.addChild(guiStaminaTxt.text);
guiStaminaTxt.x = 2048 / 2;
guiStaminaTxt.y = 1550;
LK.gui.top.addChild(guiStaminaTxt);
}
// Update stamina bar and text
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
if (guiStaminaTxt.bar) {
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
}
if (guiStaminaTxt.text) {
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// Enemy intent
enemyNode.setIntent(enemyNode.enemyData.intent);
// Render hand
renderHand();
}
// --- Draw Hand ---
function drawHand() {
hand = [];
for (var i = 0; i < 5; ++i) {
if (drawPile.length === 0) {
// Reshuffle discard
if (discard.length === 0) break;
for (var j = 0; j < discard.length; ++j) drawPile.push(cloneCard(discard[j]));
shuffleArray(drawPile);
discard = [];
}
if (drawPile.length > 0) {
hand.push(drawPile.pop());
}
}
}
// --- Render Hand ---
function renderHand() {
if (!guiHandGroup) {
guiHandGroup = new Container();
game.addChild(guiHandGroup);
}
// Remove old
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].destroy();
cardNodes = [];
// Remove old end turn button if present
if (guiHandGroup.endTurnBtn) {
guiHandGroup.endTurnBtn.destroy();
guiHandGroup.endTurnBtn = null;
}
var handY = 2400;
var handX0 = 2048 / 2 - 2 * 360;
for (var i = 0; i < hand.length; ++i) {
var cardNode = new Card();
cardNode.setCard(hand[i]);
cardNode.x = handX0 + i * 360;
cardNode.y = handY;
cardNode.onCardDown = onCardDown;
guiHandGroup.addChild(cardNode);
cardNodes.push(cardNode);
// Disable card interaction if stamina is 0
if (playerStamina <= 0) {
cardNode.interactive = false;
cardNode.alpha = 0.5;
} else {
cardNode.interactive = true;
cardNode.alpha = 1;
}
}
// Add End Turn button above cards
if (!guiHandGroup.endTurnBtn) {
var btnContainer = new Container();
// Add background asset for button
var btnBg = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 0.45,
x: 0,
y: 0
});
btnBg.alpha = playerStamina > 0 ? 0.95 : 0.7;
btnContainer.addChild(btnBg);
// Add text on top of background
var btnText = new Text2('End Turn', {
size: 64,
fill: '#000000'
});
btnText.anchor.set(0.5, 0.5);
btnText.x = 0;
btnText.y = 0;
btnContainer.addChild(btnText);
btnContainer.x = 2048 / 2;
btnContainer.y = handY - 320;
btnContainer.interactive = true;
btnContainer.buttonMode = true;
btnContainer.alpha = playerStamina > 0 ? 1 : 0.7;
btnContainer.down = function (x, y, obj) {
if (!inBattle || battleResult) return;
// Only allow end turn if player has stamina left or hand is not empty
if (playerStamina > 0 || hand.length > 0) {
// If deck is empty, rebuild with 5x Strike, 5x Defend
if (deck.length === 0) {
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[0]));
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[1]));
}
playerStamina = 0;
if (guiStaminaTxt && guiStaminaTxt.text) guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
// Disable cards
for (var i = 0; i < cardNodes.length; ++i) {
cardNodes[i].interactive = false;
cardNodes[i].alpha = 0.5;
}
// End turn, let enemy act after short delay
LK.setTimeout(enemyTurn, 400);
}
};
// Store references for later updates
btnContainer._btnBg = btnBg;
btnContainer._btnText = btnText;
guiHandGroup.endTurnBtn = btnContainer;
guiHandGroup.addChild(btnContainer);
}
// Update button color/alpha if stamina is 0
if (guiHandGroup.endTurnBtn) {
var btn = guiHandGroup.endTurnBtn;
if (btn._btnText) btn._btnText.setStyle({
fill: '#000000'
});
if (btn._btnBg) btn._btnBg.alpha = playerStamina > 0 ? 0.95 : 0.7;
btn.alpha = playerStamina > 0 ? 1 : 0.7;
}
// Update stamina bar and text after hand render
if (guiStaminaTxt && guiStaminaTxt.bar && guiStaminaTxt.text) {
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
}
// --- Card Down (Select/Drag) ---
function onCardDown(cardNode, x, y, obj) {
if (!inBattle) return;
if (battleResult) return;
if (playerStamina <= 0) return; // Prevent card play if no stamina
dragCard = cardNode;
dragStart = {
x: cardNode.x,
y: cardNode.y
};
dragValid = false;
// Highlight
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].setSelected(cardNodes[i] === cardNode);
}
// --- Game Move (Drag Card) ---
game.move = function (x, y, obj) {
if (dragCard && inBattle && !battleResult) {
// Move card with finger
dragCard.x = x;
dragCard.y = y;
var cardType = dragCard.cardData ? dragCard.cardData.type : null;
if (cardType === 'defend') {
// Defend cards: drag to player
var playerRect = {
x: playerNode.x - playerNode.width / 2,
y: playerNode.y - playerNode.height / 2,
w: playerNode.width,
h: playerNode.height
};
if (x > playerRect.x && x < playerRect.x + playerRect.w && y > playerRect.y && y < playerRect.y + playerRect.h) {
dragValid = true;
playerNode.alpha = 0.7;
} else {
dragValid = false;
playerNode.alpha = 1;
}
// Always reset enemy alpha for defend
if (enemyNode) enemyNode.alpha = 1;
} else {
// Attack/special: drag to enemy
var enemyRect = {
x: enemyNode.x - enemyNode.width / 2,
y: enemyNode.y - enemyNode.height / 2,
w: enemyNode.width,
h: enemyNode.height
};
if (x > enemyRect.x && x < enemyRect.x + enemyRect.w && y > enemyRect.y && y < enemyRect.y + enemyRect.h) {
dragValid = true;
enemyNode.alpha = 0.7;
} else {
dragValid = false;
enemyNode.alpha = 1;
}
// Always reset player alpha for attack/special
if (playerNode) playerNode.alpha = 1;
}
}
};
// --- Game Up (Release Card) ---
game.up = function (x, y, obj) {
if (dragCard && inBattle && !battleResult) {
var cardType = dragCard.cardData ? dragCard.cardData.type : null;
var valid = false;
if (cardType === 'defend') {
// Defend: check if released over player
var playerRect = {
x: playerNode.x - playerNode.width / 2,
y: playerNode.y - playerNode.height / 2,
w: playerNode.width,
h: playerNode.height
};
if (x > playerRect.x && x < playerRect.x + playerRect.w && y > playerRect.y && y < playerRect.y + playerRect.h) {
valid = true;
}
} else {
// Attack/special: check if released over enemy
var enemyRect = {
x: enemyNode.x - enemyNode.width / 2,
y: enemyNode.y - enemyNode.height / 2,
w: enemyNode.width,
h: enemyNode.height
};
if (x > enemyRect.x && x < enemyRect.x + enemyRect.w && y > enemyRect.y && y < enemyRect.y + enemyRect.h) {
valid = true;
}
}
// If valid, play card
if (valid) {
playCard(dragCard);
} else {
// Return to hand
tween(dragCard, {
x: dragStart.x,
y: dragStart.y
}, {
duration: 180,
easing: tween.easeOut
});
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].setSelected(false);
}
dragCard = null;
dragStart = null;
dragValid = false;
if (enemyNode) enemyNode.alpha = 1;
if (playerNode) playerNode.alpha = 1;
}
};
// --- Play Card ---
function playCard(cardNode) {
var idx = cardNodes.indexOf(cardNode);
if (idx === -1) return;
var card = hand[idx];
var cost = card.stamina || 1;
if (playerStamina < cost) {
// Not enough stamina, shake card and return
tween(cardNode, {
x: cardNode.x - 30
}, {
duration: 80,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
tween(cardNode, {
x: dragStart ? dragStart.x : cardNode.x
}, {
duration: 80
});
}
});
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].setSelected(false);
return;
}
playerStamina -= cost;
// Play card sound effect
LK.getSound('card').play();
// Update stamina GUI bar and text after card play
if (guiStaminaTxt && guiStaminaTxt.bar && guiStaminaTxt.text) {
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// Update player stamina text above player
if (playerNode && playerNode.staminaTxt) {
playerNode.staminaTxt.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// Animate card to enemy
tween(cardNode, {
x: enemyNode.x,
y: enemyNode.y
}, {
duration: 180,
easing: tween.easeIn,
onFinish: function onFinish() {
// Apply effect
if (card.type === 'attack') {
var dmg = card.damage;
// Bash effect: next attack +3
if (playerNode.nextAttackBonus) {
dmg += playerNode.nextAttackBonus;
playerNode.nextAttackBonus = 0;
}
enemyNode.hp -= dmg;
if (enemyNode.hp < 0) enemyNode.hp = 0;
enemyNode.updateHp();
LK.effects.flashObject(enemyNode, 0xff0000, 300);
} else if (card.type === 'defend') {
playerNode.block += card.block;
LK.effects.flashObject(playerNode, 0x55aaff, 300);
} else if (card.type === 'special') {
if (card.special === 'heal') {
playerNode.hp += card.value;
if (playerNode.hp > playerNode.maxHp) playerNode.hp = playerNode.maxHp;
LK.effects.flashObject(playerNode, 0x55ff99, 300);
} else if (card.special === 'double') {
var dmg = card.value;
if (playerNode.nextAttackBonus) {
dmg += playerNode.nextAttackBonus;
playerNode.nextAttackBonus = 0;
}
enemyNode.hp -= dmg;
enemyNode.hp -= dmg;
if (enemyNode.hp < 0) enemyNode.hp = 0;
enemyNode.updateHp();
LK.effects.flashObject(enemyNode, 0xff0000, 300);
} else if (card.special === 'bash') {
enemyNode.hp -= card.value;
if (enemyNode.hp < 0) enemyNode.hp = 0;
enemyNode.updateHp();
playerNode.nextAttackBonus = 3;
LK.effects.flashObject(enemyNode, 0xff0000, 300);
}
}
playerNode.updateStats();
// Discard card
discard.push(card);
hand.splice(idx, 1);
cardNode.destroy();
cardNodes.splice(idx, 1);
// Check win
if (enemyNode.hp <= 0) {
battleResult = 'win';
LK.setTimeout(showBattleWin, 600);
return;
}
// Enemy turn if no cards left or after play
if (hand.length === 0) {
LK.setTimeout(enemyTurn, 500);
} else {
// If stamina is 0, disable cards and prompt end turn
if (playerStamina <= 0) {
renderHand();
} else {
renderHand();
}
}
}
});
}
// --- Enemy Turn ---
function enemyTurn() {
// Enemy intent
var intent = enemyNode.intent;
if (intent && intent.type === 'attack') {
var dmg = intent.value;
var block = playerNode.block;
var taken = dmg;
if (block > 0) {
if (block >= dmg) {
playerNode.block -= dmg;
taken = 0;
} else {
taken = dmg - block;
playerNode.block = 0;
}
}
playerNode.hp -= taken;
if (playerNode.hp < 0) playerNode.hp = 0;
LK.effects.flashObject(playerNode, 0xff0000, 400);
// Show floating damage text above player
if (taken > 0) {
var dmgTxt = new Text2('-' + taken, {
size: 48,
fill: '#ff3333'
});
dmgTxt.anchor.set(0.5, 0.5);
dmgTxt.x = playerNode.x;
dmgTxt.y = playerNode.y - 120;
game.addChild(dmgTxt);
tween(dmgTxt, {
y: dmgTxt.y - 80,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
dmgTxt.destroy();
}
});
}
} else if (intent && intent.type === 'block') {
enemyNode.hp += intent.value;
if (enemyNode.hp > enemyNode.maxHp) enemyNode.hp = enemyNode.maxHp;
LK.effects.flashObject(enemyNode, 0x55aaff, 400);
}
playerNode.updateStats();
enemyNode.updateHp();
// Check lose
if (playerNode.hp <= 0) {
battleResult = 'lose';
LK.setTimeout(showBattleLose, 600);
return;
}
// Next turn: draw new hand, reset block
playerNode.block = 0;
playerNode.updateStats();
playerStamina = playerMaxStamina;
// Update stamina GUI bar and text after enemy turn
if (guiStaminaTxt && guiStaminaTxt.bar && guiStaminaTxt.text) {
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
drawHand();
renderHand();
// Update player stamina text above player
if (playerNode && playerNode.staminaTxt) {
playerNode.staminaTxt.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// New enemy intent
var enemyData = enemyNode.enemyData;
// Randomize intent
if (Math.random() < 0.5) {
enemyNode.setIntent({
type: 'attack',
value: enemyData.intent.value
});
} else {
enemyNode.setIntent({
type: 'block',
value: Math.floor(enemyData.intent.value * 0.8)
});
}
}
// --- Show Battle Win ---
function showBattleWin() {
// Remove enemy
if (enemyNode) enemyNode.destroy();
enemyNode = null;
// Hide end turn button and disable cards
if (guiHandGroup && guiHandGroup.endTurnBtn) {
guiHandGroup.endTurnBtn.visible = false;
}
for (var i = 0; i < cardNodes.length; ++i) {
cardNodes[i].interactive = false;
cardNodes[i].alpha = 0.5;
}
// Show reward
showReward();
}
// --- Show Battle Lose ---
function showBattleLose() {
LK.effects.flashScreen(0xff0000, 1000);
// Hide end turn button and disable cards
if (guiHandGroup && guiHandGroup.endTurnBtn) {
guiHandGroup.endTurnBtn.visible = false;
}
for (var i = 0; i < cardNodes.length; ++i) {
cardNodes[i].interactive = false;
cardNodes[i].alpha = 0.5;
}
LK.showGameOver();
}
// --- Show Reward (Choose 1 of 3 cards) ---
function showReward() {
if (!guiRewardGroup) {
guiRewardGroup = new Container();
game.addChild(guiRewardGroup);
}
guiRewardGroup.visible = true;
// Hide player stamina text when not in battle
if (playerNode && playerNode.staminaTxt) {
playerNode.staminaTxt.visible = false;
}
// Remove old
for (var i = 0; i < rewardCardNodes.length; ++i) rewardCardNodes[i].destroy();
rewardCardNodes = [];
if (rewardTxt) rewardTxt.destroy();
rewardTxt = new Text2('Choose a card to add to your deck\nor +1 Max Stamina', {
size: 64,
fill: '#fff'
});
rewardTxt.anchor.set(0.5, 0);
rewardTxt.x = 2048 / 2;
rewardTxt.y = 600;
guiRewardGroup.addChild(rewardTxt);
rewardCards = [];
for (var i = 0; i < 3; ++i) rewardCards.push(getRandomCard());
var rx0 = 2048 / 2 - 360;
for (var i = 0; i < 3; ++i) {
var cardNode = new Card();
cardNode.setCard(rewardCards[i]);
cardNode.x = rx0 + i * 360;
cardNode.y = 1100;
cardNode.onCardDown = onRewardCardDown;
guiRewardGroup.addChild(cardNode);
rewardCardNodes.push(cardNode);
}
// Add stamina upgrade button
if (!guiRewardGroup.staminaBtn) {
guiRewardGroup.staminaBtn = new Text2('+1 Max Stamina', {
size: 56,
fill: '#ffaa00'
});
guiRewardGroup.staminaBtn.anchor.set(0.5, 0.5);
guiRewardGroup.staminaBtn.x = 2048 / 2;
guiRewardGroup.staminaBtn.y = 1500;
guiRewardGroup.staminaBtn.interactive = true;
guiRewardGroup.staminaBtn.buttonMode = true;
guiRewardGroup.staminaBtn.down = function (x, y, obj) {
playerMaxStamina += 1;
guiRewardGroup.visible = false;
battleResult = null;
inBattle = false;
currentMapIndex += 1;
if (currentMapIndex >= mapData.length) {
LK.showYouWin();
return;
}
guiMapGroup.visible = true;
renderMap();
};
guiRewardGroup.addChild(guiRewardGroup.staminaBtn);
} else {
guiRewardGroup.staminaBtn.visible = true;
}
}
// --- Reward Card Down (Pick Card) ---
function onRewardCardDown(cardNode, x, y, obj) {
var idx = rewardCardNodes.indexOf(cardNode);
if (idx === -1) return;
// Add to deck
deck.push(cloneCard(rewardCards[idx]));
// Next map node
guiRewardGroup.visible = false;
if (game.caveBg) game.caveBg.visible = false;
if (playerNode) playerNode.visible = false;
if (playerNode && playerNode.staminaTxt) playerNode.staminaTxt.visible = false;
if (enemyNode) enemyNode.visible = false;
battleResult = null;
inBattle = false;
currentMapIndex += 1;
if (currentMapIndex >= mapData.length) {
// Win game
LK.showYouWin();
return;
}
// Show map again
if (game.mapBg) game.mapBg.visible = true;
if (game.bossBg) game.bossBg.visible = false;
guiMapGroup.visible = true;
renderMap();
}
// --- Game Update ---
game.update = function () {
// No per-frame logic needed for now
};
// --- Game Start ---
function startGame() {
// Reset state
player = null;
enemy = null;
hand = [];
deck = [];
discard = [];
drawPile = [];
selectedCard = null;
cardNodes = [];
enemyNode = null;
playerNode = null;
mapNodes = [];
mapPaths = [];
currentMapIndex = 0;
inBattle = false;
battleResult = null;
rewardCards = [];
rewardCardNodes = [];
floorNum = 1;
// Generate map
generateMap();
renderMap();
}
startGame(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Card class
var Card = Container.expand(function () {
var self = Container.call(this);
// Card data: {type, name, desc, damage, block, special}
self.cardData = null;
// Card background
var cardBg = null;
// Card text
var nameTxt = null;
var descTxt = null;
// Set card data and visuals
self.setCard = function (cardData) {
self.cardData = cardData;
if (cardBg) cardBg.destroy();
if (nameTxt) nameTxt.destroy();
if (descTxt) descTxt.destroy();
if (self.staminaTxt) {
self.staminaTxt.destroy();
self.staminaTxt = null;
}
var assetId = 'card';
if (cardData.type === 'attack') assetId = 'cardAttack';else if (cardData.type === 'defend') assetId = 'cardDefend';else if (cardData.type === 'special') assetId = 'cardSpecial';
cardBg = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
nameTxt = new Text2(cardData.name, {
size: 48,
fill: '#ffffff'
});
nameTxt.anchor.set(0.5, 0);
nameTxt.x = 0;
nameTxt.y = cardBg.height / 2 + 10;
self.addChild(nameTxt);
// Card description text (explains what the card does, e.g. "Deal 6 damage", "Gain 5 block")
// This text is shown under the card image, centered, and uses white color for clarity
descTxt = new Text2(cardData.desc, {
size: 32,
fill: '#ffffff'
});
descTxt.anchor.set(0.5, 0);
descTxt.x = 0;
descTxt.y = cardBg.height / 2 + 70;
self.addChild(descTxt);
// Show stamina cost
var cost = cardData.stamina || 1;
self.staminaTxt = new Text2(cost + '', {
size: 48,
fill: '#ffaa00'
});
self.staminaTxt.anchor.set(0.5, 0.5);
self.staminaTxt.x = 0;
self.staminaTxt.y = 160;
self.addChild(self.staminaTxt);
};
// Highlight for selection
self.setSelected = function (selected) {
if (cardBg) {
cardBg.alpha = selected ? 1 : 0.85;
cardBg.scaleX = cardBg.scaleY = selected ? 1.08 : 1;
}
};
// For drag/press
self.down = function (x, y, obj) {
if (self.onCardDown) self.onCardDown(self, x, y, obj);
};
return self;
});
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
self.maxHp = 1;
self.hp = 1;
self.intent = null; // {type, value}
self.enemyData = null;
var enemyShape = null;
var hpTxt = new Text2('', {
size: 48,
fill: '#ffffff'
});
hpTxt.anchor.set(0.5, 0.5);
hpTxt.x = 0;
hpTxt.y = 0;
self.addChild(hpTxt);
var intentTxt = new Text2('', {
size: 36,
fill: '#ffcc00'
});
intentTxt.anchor.set(0.5, 0);
intentTxt.x = 0;
intentTxt.y = 120;
self.addChild(intentTxt);
self.setEnemy = function (enemyData) {
self.enemyData = enemyData;
self.maxHp = enemyData.hp;
self.hp = enemyData.hp;
// Remove old asset if present
if (enemyShape) enemyShape.destroy();
// Use boss asset for boss enemy, otherwise normal enemy asset
var assetId = enemyData.name === 'Boss' ? 'boss' : 'enemy';
enemyShape = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.addChildAt(enemyShape, 0);
self.updateHp();
self.setIntent(enemyData.intent);
};
self.setIntent = function (intent) {
self.intent = intent;
if (intent && intent.type === 'attack') {
intentTxt.setText('Attack\n' + intent.value);
} else if (intent && intent.type === 'block') {
intentTxt.setText('Block\n' + intent.value);
} else {
intentTxt.setText('');
}
};
self.updateHp = function () {
hpTxt.setText(self.hp + ' / ' + self.maxHp);
};
return self;
});
// MapNode class
var MapNode = Container.expand(function () {
var self = Container.call(this);
self.nodeType = 'normal'; // 'normal', 'boss'
self.nodeIndex = 0;
self.isCurrent = false;
self.isVisited = false;
var nodeShape = null;
var nodeTxt = null;
self.setNode = function (type, isCurrent, isBoss) {
self.nodeType = type;
self.isCurrent = isCurrent;
if (nodeShape) nodeShape.destroy();
if (nodeTxt) nodeTxt.destroy();
var assetId = isBoss ? 'mapNodeBoss' : isCurrent ? 'mapNodeCurrent' : 'mapNode';
nodeShape = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
nodeTxt = new Text2(isBoss ? 'B' : '', {
size: 64,
fill: '#222222'
});
nodeTxt.anchor.set(0.5, 0.5);
nodeTxt.x = 0;
nodeTxt.y = 0;
self.addChild(nodeTxt);
};
// For tap
self.down = function (x, y, obj) {
if (self.onNodeDown) self.onNodeDown(self, x, y, obj);
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
self.maxHp = 30;
self.hp = 30;
self.block = 0;
var playerShape = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.4,
scaleY: 1.4
});
// Health text under player
var hpTxt = new Text2('', {
size: 48,
fill: '#ffffff'
});
hpTxt.anchor.set(0.5, 0);
hpTxt.x = 0;
hpTxt.y = 140; // Place under the player sprite
self.addChild(hpTxt);
var blockTxt = new Text2('', {
size: 36,
fill: '#55aaff'
});
blockTxt.anchor.set(0.5, 0);
blockTxt.x = 0;
blockTxt.y = 200; // Place block text further below health
self.addChild(blockTxt);
self.updateStats = function () {
hpTxt.setText(self.hp + ' / ' + self.maxHp);
blockTxt.setText('Block: ' + self.block);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// --- Card Data ---
// Card shapes (rectangles for cards)
var CARD_LIBRARY = [{
type: 'attack',
name: 'Strike',
desc: 'Deal 6 damage.',
damage: 6,
stamina: 1
}, {
type: 'defend',
name: 'Defend',
desc: 'Gain 5 block.',
block: 5,
stamina: 1
}, {
type: 'attack',
name: 'Slash',
desc: 'Deal 8 damage.',
damage: 8,
stamina: 2
}, {
type: 'defend',
name: 'Guard',
desc: 'Gain 7 block.',
block: 7,
stamina: 2
}, {
type: 'special',
name: 'Heal',
desc: 'Heal 4 HP.',
special: 'heal',
value: 4,
stamina: 2
}, {
type: 'special',
name: 'Double Strike',
desc: 'Deal 4 damage twice.',
special: 'double',
value: 4,
stamina: 2
}, {
type: 'special',
name: 'Bash',
desc: 'Deal 10 damage. Next attack +3.',
special: 'bash',
value: 10,
stamina: 2
}];
// --- Enemy Data ---
var ENEMY_LIBRARY = [{
name: 'Goblin',
hp: 18,
intent: {
type: 'attack',
value: 5
}
}, {
name: 'Slime',
hp: 22,
intent: {
type: 'block',
value: 6
}
}, {
name: 'Orc',
hp: 28,
intent: {
type: 'attack',
value: 8
}
}, {
name: 'Boss',
hp: 40,
intent: {
type: 'attack',
value: 12
}
}];
// --- Game State ---
var player = null;
var enemy = null;
var hand = [];
var deck = [];
var discard = [];
var drawPile = [];
var selectedCard = null;
var cardNodes = [];
var enemyNode = null;
var playerNode = null;
var mapNodes = [];
var mapPaths = [];
var currentMapIndex = 0;
var mapData = [];
var inBattle = false;
var battleResult = null; // null, 'win', 'lose'
var rewardCards = [];
var rewardCardNodes = [];
var rewardTxt = null;
var dragCard = null;
var dragStart = null;
var dragValid = false;
var guiHpTxt = null;
var guiEnemyHpTxt = null;
var guiFloorTxt = null;
var guiBlockTxt = null;
var guiEnemyIntentTxt = null;
var guiMapGroup = null;
var guiHandGroup = null;
var guiBattleGroup = null;
var guiRewardGroup = null;
var guiStaminaTxt = null;
var floorNum = 1;
var maxFloors = 8;
// Stamina system
var playerStamina = 3;
var playerMaxStamina = 3;
// --- Utility Functions ---
function shuffleArray(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
function cloneCard(card) {
var c = {};
for (var k in card) c[k] = card[k];
return c;
}
function getRandomCard() {
var idx = Math.floor(Math.random() * CARD_LIBRARY.length);
return cloneCard(CARD_LIBRARY[idx]);
}
function getRandomEnemy(isBoss) {
if (isBoss) return ENEMY_LIBRARY[3];
var idx = Math.floor(Math.random() * 3);
return ENEMY_LIBRARY[idx];
}
// --- Map Generation ---
function generateMap() {
// Simple linear with 2-branch at each node, boss at end
mapData = [];
for (var i = 0; i < maxFloors - 1; ++i) {
var node = {
type: 'normal',
isBoss: false,
index: i
};
mapData.push(node);
}
mapData.push({
type: 'boss',
isBoss: true,
index: maxFloors - 1
});
}
// --- Map Rendering ---
function renderMap() {
// Add map background if not present
if (!game.mapBg) {
game.mapBg = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
game.addChildAt(game.mapBg, 0);
}
game.mapBg.visible = true;
// Add boss background if not present
if (!game.bossBg) {
game.bossBg = LK.getAsset('Background', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
game.addChildAt(game.bossBg, 0);
}
// Show boss background only if on boss node, otherwise hide
if (mapData && mapData[currentMapIndex] && mapData[currentMapIndex].isBoss) {
game.bossBg.visible = true;
game.mapBg.visible = false;
} else {
game.bossBg.visible = false;
game.mapBg.visible = true;
}
if (!guiMapGroup) {
guiMapGroup = new Container();
game.addChild(guiMapGroup);
}
// Clear old
for (var i = 0; i < mapNodes.length; ++i) mapNodes[i].destroy();
mapNodes = [];
for (var i = 0; i < mapPaths.length; ++i) mapPaths[i].destroy();
mapPaths = [];
var startY = 350;
var gapY = 220;
var centerX = 2048 / 2;
for (var i = 0; i < mapData.length; ++i) {
var node = new MapNode();
var isCurrent = i === currentMapIndex;
node.setNode(mapData[i].type, isCurrent, mapData[i].isBoss);
node.x = centerX;
node.y = startY + i * gapY;
node.nodeIndex = i;
node.onNodeDown = onMapNodeDown;
guiMapGroup.addChild(node);
mapNodes.push(node);
// Path to next
if (i < mapData.length - 1) {
var path = LK.getAsset('mapPath', {
anchorX: 0.5,
anchorY: 0,
x: centerX,
y: node.y + 60,
scaleY: 1.2
});
guiMapGroup.addChild(path);
mapPaths.push(path);
}
}
}
// --- Map Node Click ---
function onMapNodeDown(node, x, y, obj) {
if (inBattle) return;
if (node.nodeIndex === currentMapIndex) {
// Enter battle
startBattle(node.nodeIndex);
}
}
// --- Start Battle ---
function startBattle(mapIdx) {
inBattle = true;
// Remove map
if (guiMapGroup) guiMapGroup.visible = false;
// Hide map and boss backgrounds, show cave background
if (game.mapBg) game.mapBg.visible = false;
if (game.bossBg) game.bossBg.visible = false;
// Setup cave background
if (!game.caveBg) {
game.caveBg = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 8,
scaleY: 8,
x: 2048 / 2,
y: 2732 / 2
});
game.addChildAt(game.caveBg, 0);
} else {
game.caveBg.visible = true;
}
// Setup player sprite on left
if (!playerNode) {
playerNode = new Player();
playerNode.x = 400;
playerNode.y = 1700;
game.addChild(playerNode);
} else {
playerNode.x = 400;
playerNode.y = 1700;
playerNode.visible = true;
}
playerNode.updateStats();
// Add stamina text above player sprite
if (!playerNode.staminaTxt) {
playerNode.staminaTxt = new Text2('', {
size: 44,
fill: '#ffaa00'
});
playerNode.staminaTxt.anchor.set(0.5, 0.5);
playerNode.staminaTxt.x = 0;
playerNode.staminaTxt.y = -160;
playerNode.addChild(playerNode.staminaTxt);
}
playerNode.staminaTxt.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
playerNode.staminaTxt.visible = true;
// Setup enemy/enemies on right
if (enemyNode) enemyNode.destroy();
enemyNode = new Enemy();
enemyNode.setEnemy(getRandomEnemy(mapData[mapIdx].isBoss));
// Set enemy height to match player height
var playerHeight = playerNode.height;
var enemyHeight = enemyNode.height;
if (enemyHeight !== 0) {
var scale = playerHeight / enemyHeight;
enemyNode.scaleX = scale;
enemyNode.scaleY = scale;
}
// Align enemy horizontally with player (same y)
enemyNode.x = 1648;
enemyNode.y = playerNode.y;
game.addChild(enemyNode);
// For future: support multiple enemies
// (for now, just one enemyNode, but can be extended to an array of Enemy nodes)
// Setup deck/hand
if (deck.length === 0) {
// Starting deck: 5x Strike, 5x Defend
deck = [];
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[0]));
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[1]));
}
drawPile = [];
for (var i = 0; i < deck.length; ++i) drawPile.push(cloneCard(deck[i]));
shuffleArray(drawPile);
discard = [];
hand = [];
drawHand();
// Show battle group
if (!guiBattleGroup) {
guiBattleGroup = new Container();
game.addChild(guiBattleGroup);
}
guiBattleGroup.visible = true;
// Hide reward group if present
if (guiRewardGroup) guiRewardGroup.visible = false;
// Floor text
if (!guiFloorTxt) {
guiFloorTxt = new Text2('', {
size: 64,
fill: '#ffffff'
});
guiFloorTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(guiFloorTxt);
}
guiFloorTxt.setText('Floor ' + (mapIdx + 1));
// Reset player stats
playerNode.block = 0;
playerNode.updateStats();
// Reset stamina
playerMaxStamina = 3 + Math.floor(currentMapIndex / 2); // Optionally scale with progress
playerStamina = playerMaxStamina;
// Show stamina UI as a bar and text above cards, visually distinct
if (!guiStaminaTxt) {
// Stamina bar background
guiStaminaTxt = new Container();
guiStaminaTxt.bg = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 0.25,
x: 0,
y: 0
});
guiStaminaTxt.bg.alpha = 0.18;
guiStaminaTxt.addChild(guiStaminaTxt.bg);
// Stamina bar fill
guiStaminaTxt.bar = LK.getAsset('cardAttack', {
anchorX: 0,
anchorY: 0.5,
scaleX: 2.1,
scaleY: 0.18,
x: -160,
y: 0
});
guiStaminaTxt.bar.alpha = 0.45;
guiStaminaTxt.addChild(guiStaminaTxt.bar);
// Stamina text
guiStaminaTxt.text = new Text2('', {
size: 54,
fill: '#ffaa00'
});
guiStaminaTxt.text.anchor.set(0.5, 0.5);
guiStaminaTxt.text.x = 0;
guiStaminaTxt.text.y = 0;
guiStaminaTxt.addChild(guiStaminaTxt.text);
guiStaminaTxt.x = 2048 / 2;
guiStaminaTxt.y = 1550;
LK.gui.top.addChild(guiStaminaTxt);
}
// Update stamina bar and text
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
if (guiStaminaTxt.bar) {
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
}
if (guiStaminaTxt.text) {
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// Enemy intent
enemyNode.setIntent(enemyNode.enemyData.intent);
// Render hand
renderHand();
}
// --- Draw Hand ---
function drawHand() {
hand = [];
for (var i = 0; i < 5; ++i) {
if (drawPile.length === 0) {
// Reshuffle discard
if (discard.length === 0) break;
for (var j = 0; j < discard.length; ++j) drawPile.push(cloneCard(discard[j]));
shuffleArray(drawPile);
discard = [];
}
if (drawPile.length > 0) {
hand.push(drawPile.pop());
}
}
}
// --- Render Hand ---
function renderHand() {
if (!guiHandGroup) {
guiHandGroup = new Container();
game.addChild(guiHandGroup);
}
// Remove old
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].destroy();
cardNodes = [];
// Remove old end turn button if present
if (guiHandGroup.endTurnBtn) {
guiHandGroup.endTurnBtn.destroy();
guiHandGroup.endTurnBtn = null;
}
var handY = 2400;
var handX0 = 2048 / 2 - 2 * 360;
for (var i = 0; i < hand.length; ++i) {
var cardNode = new Card();
cardNode.setCard(hand[i]);
cardNode.x = handX0 + i * 360;
cardNode.y = handY;
cardNode.onCardDown = onCardDown;
guiHandGroup.addChild(cardNode);
cardNodes.push(cardNode);
// Disable card interaction if stamina is 0
if (playerStamina <= 0) {
cardNode.interactive = false;
cardNode.alpha = 0.5;
} else {
cardNode.interactive = true;
cardNode.alpha = 1;
}
}
// Add End Turn button above cards
if (!guiHandGroup.endTurnBtn) {
var btnContainer = new Container();
// Add background asset for button
var btnBg = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 0.45,
x: 0,
y: 0
});
btnBg.alpha = playerStamina > 0 ? 0.95 : 0.7;
btnContainer.addChild(btnBg);
// Add text on top of background
var btnText = new Text2('End Turn', {
size: 64,
fill: '#000000'
});
btnText.anchor.set(0.5, 0.5);
btnText.x = 0;
btnText.y = 0;
btnContainer.addChild(btnText);
btnContainer.x = 2048 / 2;
btnContainer.y = handY - 320;
btnContainer.interactive = true;
btnContainer.buttonMode = true;
btnContainer.alpha = playerStamina > 0 ? 1 : 0.7;
btnContainer.down = function (x, y, obj) {
if (!inBattle || battleResult) return;
// Only allow end turn if player has stamina left or hand is not empty
if (playerStamina > 0 || hand.length > 0) {
// If deck is empty, rebuild with 5x Strike, 5x Defend
if (deck.length === 0) {
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[0]));
for (var i = 0; i < 5; ++i) deck.push(cloneCard(CARD_LIBRARY[1]));
}
playerStamina = 0;
if (guiStaminaTxt && guiStaminaTxt.text) guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
// Disable cards
for (var i = 0; i < cardNodes.length; ++i) {
cardNodes[i].interactive = false;
cardNodes[i].alpha = 0.5;
}
// End turn, let enemy act after short delay
LK.setTimeout(enemyTurn, 400);
}
};
// Store references for later updates
btnContainer._btnBg = btnBg;
btnContainer._btnText = btnText;
guiHandGroup.endTurnBtn = btnContainer;
guiHandGroup.addChild(btnContainer);
}
// Update button color/alpha if stamina is 0
if (guiHandGroup.endTurnBtn) {
var btn = guiHandGroup.endTurnBtn;
if (btn._btnText) btn._btnText.setStyle({
fill: '#000000'
});
if (btn._btnBg) btn._btnBg.alpha = playerStamina > 0 ? 0.95 : 0.7;
btn.alpha = playerStamina > 0 ? 1 : 0.7;
}
// Update stamina bar and text after hand render
if (guiStaminaTxt && guiStaminaTxt.bar && guiStaminaTxt.text) {
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
}
// --- Card Down (Select/Drag) ---
function onCardDown(cardNode, x, y, obj) {
if (!inBattle) return;
if (battleResult) return;
if (playerStamina <= 0) return; // Prevent card play if no stamina
dragCard = cardNode;
dragStart = {
x: cardNode.x,
y: cardNode.y
};
dragValid = false;
// Highlight
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].setSelected(cardNodes[i] === cardNode);
}
// --- Game Move (Drag Card) ---
game.move = function (x, y, obj) {
if (dragCard && inBattle && !battleResult) {
// Move card with finger
dragCard.x = x;
dragCard.y = y;
var cardType = dragCard.cardData ? dragCard.cardData.type : null;
if (cardType === 'defend') {
// Defend cards: drag to player
var playerRect = {
x: playerNode.x - playerNode.width / 2,
y: playerNode.y - playerNode.height / 2,
w: playerNode.width,
h: playerNode.height
};
if (x > playerRect.x && x < playerRect.x + playerRect.w && y > playerRect.y && y < playerRect.y + playerRect.h) {
dragValid = true;
playerNode.alpha = 0.7;
} else {
dragValid = false;
playerNode.alpha = 1;
}
// Always reset enemy alpha for defend
if (enemyNode) enemyNode.alpha = 1;
} else {
// Attack/special: drag to enemy
var enemyRect = {
x: enemyNode.x - enemyNode.width / 2,
y: enemyNode.y - enemyNode.height / 2,
w: enemyNode.width,
h: enemyNode.height
};
if (x > enemyRect.x && x < enemyRect.x + enemyRect.w && y > enemyRect.y && y < enemyRect.y + enemyRect.h) {
dragValid = true;
enemyNode.alpha = 0.7;
} else {
dragValid = false;
enemyNode.alpha = 1;
}
// Always reset player alpha for attack/special
if (playerNode) playerNode.alpha = 1;
}
}
};
// --- Game Up (Release Card) ---
game.up = function (x, y, obj) {
if (dragCard && inBattle && !battleResult) {
var cardType = dragCard.cardData ? dragCard.cardData.type : null;
var valid = false;
if (cardType === 'defend') {
// Defend: check if released over player
var playerRect = {
x: playerNode.x - playerNode.width / 2,
y: playerNode.y - playerNode.height / 2,
w: playerNode.width,
h: playerNode.height
};
if (x > playerRect.x && x < playerRect.x + playerRect.w && y > playerRect.y && y < playerRect.y + playerRect.h) {
valid = true;
}
} else {
// Attack/special: check if released over enemy
var enemyRect = {
x: enemyNode.x - enemyNode.width / 2,
y: enemyNode.y - enemyNode.height / 2,
w: enemyNode.width,
h: enemyNode.height
};
if (x > enemyRect.x && x < enemyRect.x + enemyRect.w && y > enemyRect.y && y < enemyRect.y + enemyRect.h) {
valid = true;
}
}
// If valid, play card
if (valid) {
playCard(dragCard);
} else {
// Return to hand
tween(dragCard, {
x: dragStart.x,
y: dragStart.y
}, {
duration: 180,
easing: tween.easeOut
});
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].setSelected(false);
}
dragCard = null;
dragStart = null;
dragValid = false;
if (enemyNode) enemyNode.alpha = 1;
if (playerNode) playerNode.alpha = 1;
}
};
// --- Play Card ---
function playCard(cardNode) {
var idx = cardNodes.indexOf(cardNode);
if (idx === -1) return;
var card = hand[idx];
var cost = card.stamina || 1;
if (playerStamina < cost) {
// Not enough stamina, shake card and return
tween(cardNode, {
x: cardNode.x - 30
}, {
duration: 80,
yoyo: true,
repeat: 1,
onFinish: function onFinish() {
tween(cardNode, {
x: dragStart ? dragStart.x : cardNode.x
}, {
duration: 80
});
}
});
for (var i = 0; i < cardNodes.length; ++i) cardNodes[i].setSelected(false);
return;
}
playerStamina -= cost;
// Play card sound effect
LK.getSound('card').play();
// Update stamina GUI bar and text after card play
if (guiStaminaTxt && guiStaminaTxt.bar && guiStaminaTxt.text) {
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// Update player stamina text above player
if (playerNode && playerNode.staminaTxt) {
playerNode.staminaTxt.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// Animate card to enemy
tween(cardNode, {
x: enemyNode.x,
y: enemyNode.y
}, {
duration: 180,
easing: tween.easeIn,
onFinish: function onFinish() {
// Apply effect
if (card.type === 'attack') {
var dmg = card.damage;
// Bash effect: next attack +3
if (playerNode.nextAttackBonus) {
dmg += playerNode.nextAttackBonus;
playerNode.nextAttackBonus = 0;
}
enemyNode.hp -= dmg;
if (enemyNode.hp < 0) enemyNode.hp = 0;
enemyNode.updateHp();
LK.effects.flashObject(enemyNode, 0xff0000, 300);
} else if (card.type === 'defend') {
playerNode.block += card.block;
LK.effects.flashObject(playerNode, 0x55aaff, 300);
} else if (card.type === 'special') {
if (card.special === 'heal') {
playerNode.hp += card.value;
if (playerNode.hp > playerNode.maxHp) playerNode.hp = playerNode.maxHp;
LK.effects.flashObject(playerNode, 0x55ff99, 300);
} else if (card.special === 'double') {
var dmg = card.value;
if (playerNode.nextAttackBonus) {
dmg += playerNode.nextAttackBonus;
playerNode.nextAttackBonus = 0;
}
enemyNode.hp -= dmg;
enemyNode.hp -= dmg;
if (enemyNode.hp < 0) enemyNode.hp = 0;
enemyNode.updateHp();
LK.effects.flashObject(enemyNode, 0xff0000, 300);
} else if (card.special === 'bash') {
enemyNode.hp -= card.value;
if (enemyNode.hp < 0) enemyNode.hp = 0;
enemyNode.updateHp();
playerNode.nextAttackBonus = 3;
LK.effects.flashObject(enemyNode, 0xff0000, 300);
}
}
playerNode.updateStats();
// Discard card
discard.push(card);
hand.splice(idx, 1);
cardNode.destroy();
cardNodes.splice(idx, 1);
// Check win
if (enemyNode.hp <= 0) {
battleResult = 'win';
LK.setTimeout(showBattleWin, 600);
return;
}
// Enemy turn if no cards left or after play
if (hand.length === 0) {
LK.setTimeout(enemyTurn, 500);
} else {
// If stamina is 0, disable cards and prompt end turn
if (playerStamina <= 0) {
renderHand();
} else {
renderHand();
}
}
}
});
}
// --- Enemy Turn ---
function enemyTurn() {
// Enemy intent
var intent = enemyNode.intent;
if (intent && intent.type === 'attack') {
var dmg = intent.value;
var block = playerNode.block;
var taken = dmg;
if (block > 0) {
if (block >= dmg) {
playerNode.block -= dmg;
taken = 0;
} else {
taken = dmg - block;
playerNode.block = 0;
}
}
playerNode.hp -= taken;
if (playerNode.hp < 0) playerNode.hp = 0;
LK.effects.flashObject(playerNode, 0xff0000, 400);
// Show floating damage text above player
if (taken > 0) {
var dmgTxt = new Text2('-' + taken, {
size: 48,
fill: '#ff3333'
});
dmgTxt.anchor.set(0.5, 0.5);
dmgTxt.x = playerNode.x;
dmgTxt.y = playerNode.y - 120;
game.addChild(dmgTxt);
tween(dmgTxt, {
y: dmgTxt.y - 80,
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
dmgTxt.destroy();
}
});
}
} else if (intent && intent.type === 'block') {
enemyNode.hp += intent.value;
if (enemyNode.hp > enemyNode.maxHp) enemyNode.hp = enemyNode.maxHp;
LK.effects.flashObject(enemyNode, 0x55aaff, 400);
}
playerNode.updateStats();
enemyNode.updateHp();
// Check lose
if (playerNode.hp <= 0) {
battleResult = 'lose';
LK.setTimeout(showBattleLose, 600);
return;
}
// Next turn: draw new hand, reset block
playerNode.block = 0;
playerNode.updateStats();
playerStamina = playerMaxStamina;
// Update stamina GUI bar and text after enemy turn
if (guiStaminaTxt && guiStaminaTxt.bar && guiStaminaTxt.text) {
var staminaRatio = playerMaxStamina > 0 ? playerStamina / playerMaxStamina : 0;
guiStaminaTxt.bar.scaleX = 2.1 * staminaRatio;
guiStaminaTxt.text.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
drawHand();
renderHand();
// Update player stamina text above player
if (playerNode && playerNode.staminaTxt) {
playerNode.staminaTxt.setText('Stamina: ' + playerStamina + ' / ' + playerMaxStamina);
}
// New enemy intent
var enemyData = enemyNode.enemyData;
// Randomize intent
if (Math.random() < 0.5) {
enemyNode.setIntent({
type: 'attack',
value: enemyData.intent.value
});
} else {
enemyNode.setIntent({
type: 'block',
value: Math.floor(enemyData.intent.value * 0.8)
});
}
}
// --- Show Battle Win ---
function showBattleWin() {
// Remove enemy
if (enemyNode) enemyNode.destroy();
enemyNode = null;
// Hide end turn button and disable cards
if (guiHandGroup && guiHandGroup.endTurnBtn) {
guiHandGroup.endTurnBtn.visible = false;
}
for (var i = 0; i < cardNodes.length; ++i) {
cardNodes[i].interactive = false;
cardNodes[i].alpha = 0.5;
}
// Show reward
showReward();
}
// --- Show Battle Lose ---
function showBattleLose() {
LK.effects.flashScreen(0xff0000, 1000);
// Hide end turn button and disable cards
if (guiHandGroup && guiHandGroup.endTurnBtn) {
guiHandGroup.endTurnBtn.visible = false;
}
for (var i = 0; i < cardNodes.length; ++i) {
cardNodes[i].interactive = false;
cardNodes[i].alpha = 0.5;
}
LK.showGameOver();
}
// --- Show Reward (Choose 1 of 3 cards) ---
function showReward() {
if (!guiRewardGroup) {
guiRewardGroup = new Container();
game.addChild(guiRewardGroup);
}
guiRewardGroup.visible = true;
// Hide player stamina text when not in battle
if (playerNode && playerNode.staminaTxt) {
playerNode.staminaTxt.visible = false;
}
// Remove old
for (var i = 0; i < rewardCardNodes.length; ++i) rewardCardNodes[i].destroy();
rewardCardNodes = [];
if (rewardTxt) rewardTxt.destroy();
rewardTxt = new Text2('Choose a card to add to your deck\nor +1 Max Stamina', {
size: 64,
fill: '#fff'
});
rewardTxt.anchor.set(0.5, 0);
rewardTxt.x = 2048 / 2;
rewardTxt.y = 600;
guiRewardGroup.addChild(rewardTxt);
rewardCards = [];
for (var i = 0; i < 3; ++i) rewardCards.push(getRandomCard());
var rx0 = 2048 / 2 - 360;
for (var i = 0; i < 3; ++i) {
var cardNode = new Card();
cardNode.setCard(rewardCards[i]);
cardNode.x = rx0 + i * 360;
cardNode.y = 1100;
cardNode.onCardDown = onRewardCardDown;
guiRewardGroup.addChild(cardNode);
rewardCardNodes.push(cardNode);
}
// Add stamina upgrade button
if (!guiRewardGroup.staminaBtn) {
guiRewardGroup.staminaBtn = new Text2('+1 Max Stamina', {
size: 56,
fill: '#ffaa00'
});
guiRewardGroup.staminaBtn.anchor.set(0.5, 0.5);
guiRewardGroup.staminaBtn.x = 2048 / 2;
guiRewardGroup.staminaBtn.y = 1500;
guiRewardGroup.staminaBtn.interactive = true;
guiRewardGroup.staminaBtn.buttonMode = true;
guiRewardGroup.staminaBtn.down = function (x, y, obj) {
playerMaxStamina += 1;
guiRewardGroup.visible = false;
battleResult = null;
inBattle = false;
currentMapIndex += 1;
if (currentMapIndex >= mapData.length) {
LK.showYouWin();
return;
}
guiMapGroup.visible = true;
renderMap();
};
guiRewardGroup.addChild(guiRewardGroup.staminaBtn);
} else {
guiRewardGroup.staminaBtn.visible = true;
}
}
// --- Reward Card Down (Pick Card) ---
function onRewardCardDown(cardNode, x, y, obj) {
var idx = rewardCardNodes.indexOf(cardNode);
if (idx === -1) return;
// Add to deck
deck.push(cloneCard(rewardCards[idx]));
// Next map node
guiRewardGroup.visible = false;
if (game.caveBg) game.caveBg.visible = false;
if (playerNode) playerNode.visible = false;
if (playerNode && playerNode.staminaTxt) playerNode.staminaTxt.visible = false;
if (enemyNode) enemyNode.visible = false;
battleResult = null;
inBattle = false;
currentMapIndex += 1;
if (currentMapIndex >= mapData.length) {
// Win game
LK.showYouWin();
return;
}
// Show map again
if (game.mapBg) game.mapBg.visible = true;
if (game.bossBg) game.bossBg.visible = false;
guiMapGroup.visible = true;
renderMap();
}
// --- Game Update ---
game.update = function () {
// No per-frame logic needed for now
};
// --- Game Start ---
function startGame() {
// Reset state
player = null;
enemy = null;
hand = [];
deck = [];
discard = [];
drawPile = [];
selectedCard = null;
cardNodes = [];
enemyNode = null;
playerNode = null;
mapNodes = [];
mapPaths = [];
currentMapIndex = 0;
inBattle = false;
battleResult = null;
rewardCards = [];
rewardCardNodes = [];
floorNum = 1;
// Generate map
generateMap();
renderMap();
}
startGame();
2d stylized dungeon enemy. In-Game asset. 2d. High contrast. No shadows
boss logo skull head. In-Game asset. 2d. High contrast. No shadows
dungeon card game, Strike card. In-Game asset. 2d. High contrast. No shadows
dungeon card game defend card . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
dungeon card game special card with fire logo . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
2d card game player hero asset. In-Game asset. 2d. High contrast. No shadows
dungeon simple 2d side view dungeon crawler game background. In-Game asset. 2d. High contrast. No shadows. background
golden button horizontal. In-Game asset. 2d. High contrast. No shadows
2d dungeon crawler boss. In-Game asset. 2d. High contrast. No shadows