/**** * 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