User prompt
Dungeon Deck Crawler
Initial prompt
2d Card game like Slay the spire the player has 5 cards on his screen and every card has abilities like attack defend (can be vary) and it is a dungeon crawler roguelike game so player moves to the next cave (player can choose the next cave on map) and there will be variaty of monsters in caves and player has to defeat them with cards which choosed
/**** * 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