Code edit (2 edits merged)
Please save this source code
User prompt
Update as needed with: // Check if AI side boss is defeated (only in PvP) if (gameMode === 'pvp' && !this.aiBossDefeated && activeAIChips.length === 0) { this.aiBossDefeated = true; createFloatingText('AI ' + bossType + ' DEFEATED!', SCREEN_WIDTH / 2, AI_AREA_Y + SLOT_HEIGHT, 0xffd700); // Show roulette for next boss wave if player boss is also defeated if (this.playerBossDefeated) { this.showRouletteForNextBoss(); } }
User prompt
Update as needed with: // Check if player side boss is defeated if (!this.playerBossDefeated && activePlayerChips.length === 0) { this.playerBossDefeated = true; createFloatingText(bossType + ' DEFEATED!', SCREEN_WIDTH / 2, PLAYER_AREA_Y + SLOT_HEIGHT, 0xffd700); // Show roulette for the NEXT boss wave (when both bosses are defeated) if (gameMode === 'pvp' && this.aiBossDefeated) { // Both defeated, show roulette for next boss wave this.showRouletteForNextBoss(); } else if (gameMode !== 'pvp') { // In single player modes, show roulette immediately this.showRouletteForNextBoss(); } }
User prompt
Update as needed with: // Check if both bosses were defeated in a boss wave if (this.isBossWave(this.waveNumber) && this.playerBossDefeated && this.aiBossDefeated) { var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS"; console.log("Both " + bossType + "es defeated! Moving to next round!"); createFloatingText('ROUND COMPLETE!', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0x00ff00); // Reset boss defeat flags this.playerBossDefeated = false; this.aiBossDefeated = false; } var nextWave = this.waveNumber + 1; this.waveTimer = 0; this.waveNumber = nextWave; this.playerSpawnTimer = 0; this.aiSpawnTimer = 0; this.bossSpawned = false; // Reset boss spawn flag // Check if the next wave is a boss wave and show roulette if needed if (this.isBossWave(this.waveNumber)) { BossSystem.showBossRoulette(function() { // Roulette complete, continue with normal wave logic console.log("Boss roulette completed for wave " + WaveSystem.waveNumber); }); }
User prompt
Update as needed with: // In PokerChip activate method, modify the boss setup section: // NOW handle boss-specific setup BEFORE setChipAppearance if (self.isBoss && typeof gameMode !== 'undefined' && gameMode === 'pvp' && typeof BossSystem !== 'undefined' && BossSystem.currentBossType) { self.bossType = BossSystem.currentBossType.id; self.bossAbilityTimer = 0; self.bossSpeedStartTime = LK.ticks; self.bossOriginalSpeed = self.speed; console.log("Boss activated with type:", self.bossType, "Wave:", WaveSystem.waveNumber); // Apply boss abilities for both mini-bosses and main bosses // Speed boss starts slower if (self.bossType === 'speed') { self.speed = self.speed * 0.5; } // Immune boss starts with random suit immunity if (self.bossType === 'immune') { var suits = ['hearts', 'diamonds', 'clubs', 'spades']; self.bossImmuneToSuit = suits[Math.floor(Math.random() * suits.length)]; self.bossImmuneTimer = 0; } // Lock boss locks a random card if (self.bossType === 'lock') { if (typeof self.lockRandomCard === 'function') { self.lockRandomCard(); } } }
User prompt
Update as needed with: // In showBossRoulette, modify the title to show mini-boss vs boss: showBossRoulette: function showBossRoulette(callback) { var nextBossWave = this.getNextBossWave(); // Determine if it's a mini-boss or main boss var isMiniBoss = nextBossWave % 10 === 5; var bossTypeText = isMiniBoss ? "MINI-BOSS" : "BOSS"; this.rouletteContainer = new Container(); uiLayer.addChild(this.rouletteContainer); // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.8; this.rouletteContainer.addChild(bg); // Title - show whether it's mini-boss or main boss var title = new Text2(bossTypeText + ' INCOMING', { size: 120, fill: isMiniBoss ? 0xff8800 : 0xff0000, // Orange for mini-boss, red for main boss weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 400; this.rouletteContainer.addChild(title); // ... rest of the roulette logic remains the same, but update the final title change: // In the onFinish callback where the title changes: onFinish: function() { // Change title to show wave number and type title.setText('WAVE ' + nextBossWave + ' ' + bossTypeText); title.fill = 0xffff00; // Change to yellow // ... rest remains the same } },
User prompt
Update as needed with: // In BossSystem, modify getNextBossWave to handle mini-bosses: getNextBossWave: function getNextBossWave() { var currentWave = WaveSystem.waveNumber; // Find the next boss wave (either mini-boss at x5 or main boss at x0) var nextBossWave; if (currentWave % 10 === 0) { // Just finished a main boss, next boss is at +5 (mini-boss) nextBossWave = currentWave + 5; } else if (currentWave % 10 === 5) { // Just finished a mini-boss, next boss is at +5 (main boss) nextBossWave = currentWave + 5; } else { // Find next boss wave (either mini or main) var remainder = currentWave % 10; if (remainder < 5) { nextBossWave = currentWave + (5 - remainder); // Next mini-boss } else { nextBossWave = currentWave + (10 - remainder); // Next main boss } } return nextBossWave; },
User prompt
Update as needed with: // In WaveSystem.update(), modify the boss spawning section: if (this.isBossWave(this.waveNumber)) { if (!this.bossSpawned && this.waveTimer === 1) { var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS"; console.log("=== " + bossType + " WAVE " + this.waveNumber + " STARTING ==="); // MODIFIED: Set the boss type from stored selection for BOTH mini-bosses and main bosses BossSystem.currentBossType = BossSystem.getBossTypeForWave(this.waveNumber); console.log("Setting boss type for wave " + this.waveNumber + ":", BossSystem.currentBossType); // Spawn bosses normally var playerBossValue = this.getBossChipValue(this.waveNumber); ChipSpawner.spawnChip(playerBossValue, true); if (gameMode === 'pvp') { var aiBossValue = this.getBossChipValue(this.waveNumber); ChipSpawner.spawnChip(aiBossValue, false); } else { this.aiBossDefeated = true; } this.bossSpawned = true; return; } // ... rest of boss wave logic remains the same }
User prompt
Update as needed with: // In WaveSystem.update(), modify the boss defeat detection section: if (this.bossSpawned && this.waveTimer > 120) { var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS"; // Check if player side boss is defeated if (!this.playerBossDefeated && activePlayerChips.length === 0) { this.playerBossDefeated = true; createFloatingText(bossType + ' DEFEATED!', SCREEN_WIDTH / 2, PLAYER_AREA_Y + SLOT_HEIGHT, 0xffd700); // NEW: Show roulette for next boss wave (both mini-boss and main boss) if (this.isBossWave(this.waveNumber + 1)) { // Next wave is also a boss wave, show roulette BossSystem.showBossRoulette(function () { // Just continue - don't spawn anything }); } } // Check if AI side boss is defeated (only in PvP) if (gameMode === 'pvp' && !this.aiBossDefeated && activeAIChips.length === 0) { this.aiBossDefeated = true; createFloatingText('AI ' + bossType + ' DEFEATED!', SCREEN_WIDTH / 2, AI_AREA_Y + SLOT_HEIGHT, 0xffd700); } // If both bosses defeated, immediately end the wave if (this.playerBossDefeated && this.aiBossDefeated) { this.waveTimer = this.waveDuration - 1; } }
User prompt
Update as needed with: self.setChipAppearance = function () { // Hide previous graphic and reset its scale to default if it exists if (chipGraphics) { chipGraphics.visible = false; chipGraphics.scale.set(1); } // For boss chips in PvP mode, use the specific boss asset if (self.isBoss && typeof gameMode !== 'undefined' && gameMode === 'pvp' && self.bossType) { console.log("Setting boss appearance for type:", self.bossType); // Find the boss type data to get the correct asset var bossTypeData = BossSystem.bossTypes.find(function(type) { return type.id === self.bossType; }); if (bossTypeData) { // Create the boss chip graphic if it doesn't exist in our assets collection if (!chipGraphicsAssets[bossTypeData.chipAsset]) { chipGraphicsAssets[bossTypeData.chipAsset] = self.attachAsset(bossTypeData.chipAsset, { anchorX: 0.5, anchorY: 0.5 }); chipGraphicsAssets[bossTypeData.chipAsset].visible = false; } chipGraphics = chipGraphicsAssets[bossTypeData.chipAsset]; console.log("Using boss asset:", bossTypeData.chipAsset); } else { console.log("Boss type data not found, using fallback"); chipGraphics = chipGraphicsAssets[self.value] || chipGraphicsAssets[1]; } } else { // Regular chip logic chipGraphics = chipGraphicsAssets[self.value] || chipGraphicsAssets[1]; } if (chipGraphics) { chipGraphics.visible = true; // Apply boss scaling if (self.isBoss) { chipGraphics.scale.set(1.25); } } self.updateHealthText(); };
User prompt
Update as needed with: self.activate = function (value, isPlayerSide, startPos) { self.active = true; self.visible = true; self.value = value; self.isPlayerSide = isPlayerSide; // Mark boss chips (they have much higher values than normal chips) self.isBoss = value >= 100; // Initialize boss properties self.bossType = null; self.bossAbilityTimer = 0; self.bossImmuneToSuit = null; self.bossImmuneTimer = 0; self.bossScrambleCount = 0; self.bossRainTimer = 0; self.bossLockedCard = null; self.bossLockGraphic = null; self.bossSpeedStartTime = 0; self.bossOriginalSpeed = 0; // Health calculations self.maxHealth = value * 50; var healthMultiplier = Math.pow(1.5, Math.floor((WaveSystem.waveNumber - 1) / 5)); self.maxHealth = self.maxHealth * healthMultiplier; if (self.isBoss) { self.maxHealth *= 0.7; if (typeof gameMode !== 'undefined' && gameMode === 'pvp') { self.maxHealth *= 1.2; } } self.health = self.maxHealth; self.pathProgress = 0; // Reset status effects self.burnDamage = 0; self.burnDuration = 0; self.burnTickTimer = 0; self.burnSourceIsPlayer = null; self.slowDuration = 0; self.slowAmount = 0; self.freezeDuration = 0; self.originalSpeed = 0; if (self.iceCube) { self.iceCube.visible = false; } if (self.slowEffectIcon) { self.slowEffectIcon.visible = false; } self.freezeImmunityTimer = 0; // Set speed self.speed = 0.03; self.damageFlashTimer = 0; // Recreate healthText if (healthText && healthText.parent) { healthText.parent.removeChild(healthText); } healthText = new Text2('', { size: 80, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 8 }); healthText.anchor.set(0.5, 0.5); self.addChild(healthText); // NOW handle boss-specific setup BEFORE setChipAppearance if (self.isBoss && typeof gameMode !== 'undefined' && gameMode === 'pvp' && typeof BossSystem !== 'undefined' && BossSystem.currentBossType) { self.bossType = BossSystem.currentBossType.id; self.bossAbilityTimer = 0; self.bossSpeedStartTime = LK.ticks; self.bossOriginalSpeed = self.speed; console.log("Boss activated with type:", self.bossType); // Speed boss starts slower if (self.bossType === 'speed') { self.speed = self.speed * 0.5; } // Immune boss starts with random suit immunity if (self.bossType === 'immune') { var suits = ['hearts', 'diamonds', 'clubs', 'spades']; self.bossImmuneToSuit = suits[Math.floor(Math.random() * suits.length)]; self.bossImmuneTimer = 0; } // Lock boss locks a random card if (self.bossType === 'lock') { if (typeof self.lockRandomCard === 'function') { self.lockRandomCard(); } } } // Set appearance AFTER boss properties are set self.setChipAppearance(); self.x = startPos.x; self.y = startPos.y; };
User prompt
Update as needed with: // In WaveSystem.update(), modify the boss spawning section: if (this.isBossWave(this.waveNumber)) { if (!this.bossSpawned && this.waveTimer === 1) { var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS"; console.log("=== " + bossType + " WAVE " + this.waveNumber + " STARTING ==="); // MODIFIED: Set the boss type from stored selection if (bossType === "BOSS") { BossSystem.currentBossType = BossSystem.getBossTypeForWave(this.waveNumber); console.log("Setting boss type for wave " + this.waveNumber + ":", BossSystem.currentBossType); } else { BossSystem.currentBossType = null; // Mini-bosses don't have special abilities } // Spawn bosses normally var playerBossValue = this.getBossChipValue(this.waveNumber); ChipSpawner.spawnChip(playerBossValue, true); if (gameMode === 'pvp') { var aiBossValue = this.getBossChipValue(this.waveNumber); ChipSpawner.spawnChip(aiBossValue, false); } else { this.aiBossDefeated = true; } this.bossSpawned = true; return; } // ... rest of boss wave logic }
User prompt
Update with: // Remove or comment out the showBossSelection function since it's now integrated /* showBossSelection: function showBossSelection(bossType, waveNumber, callback) { // This function is no longer needed } */
User prompt
Update as needed with: showBossRoulette: function showBossRoulette(callback) { var nextBossWave = this.getNextBossWave(); this.rouletteContainer = new Container(); uiLayer.addChild(this.rouletteContainer); // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.8; this.rouletteContainer.addChild(bg); // Title var title = new Text2('BOSS INCOMING', { size: 120, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 400; this.rouletteContainer.addChild(title); // Horizontal scrolling container var scrollContainer = new Container(); scrollContainer.x = 0; scrollContainer.y = SCREEN_HEIGHT / 2; this.rouletteContainer.addChild(scrollContainer); // Create a long row of boss chips var chipSpacing = 300; var totalChips = 50; var bossChips = []; // Determine which boss will be selected var finalBossType = this.selectRandomBossType(); var finalIndex = this.bossTypes.indexOf(finalBossType); // Calculate positioning var centerX = SCREEN_WIDTH / 2; var finalChipIndex = Math.floor(totalChips * 0.7); // Create the scrolling chips for (var i = 0; i < totalChips; i++) { var chipContainer = new Container(); chipContainer.x = i * chipSpacing; chipContainer.y = 0; scrollContainer.addChild(chipContainer); // Determine which boss type this chip represents var bossTypeIndex; if (i === finalChipIndex) { bossTypeIndex = finalIndex; } else { bossTypeIndex = Math.floor(Math.random() * this.bossTypes.length); } var bossType = this.bossTypes[bossTypeIndex]; // Boss chip graphic var chip = LK.getAsset(bossType.chipAsset, { anchorX: 0.5, anchorY: 0.5 }); chip.scale.set(2); chipContainer.addChild(chip); // Store reference to the final chip if (i === finalChipIndex) { chipContainer.isFinalChip = true; chipContainer.bossType = bossType; } bossChips.push(chipContainer); } // Start the container off-screen to the left scrollContainer.x = -SCREEN_WIDTH; // Calculate final position so the selected chip ends up in center var finalScrollX = centerX - (finalChipIndex * chipSpacing); // Animate the scrolling var scrollDuration = 3000; var self = this; tween(scrollContainer, { x: finalScrollX }, { duration: scrollDuration, easing: tween.quadOut, onFinish: function() { // Find the final chip var selectedChip = null; for (var i = 0; i < bossChips.length; i++) { if (bossChips[i].isFinalChip) { selectedChip = bossChips[i]; break; } } if (selectedChip) { // Fade out all other chips for (var i = 0; i < bossChips.length; i++) { if (!bossChips[i].isFinalChip) { tween(bossChips[i], { alpha: 0 }, { duration: 500 }); } } // Scale up and highlight the selected chip var selectedBossChip = selectedChip.children[0]; selectedBossChip.tint = 0xffff00; tween(selectedChip, { scaleX: 2.0, // Scale up more dramatically scaleY: 2.0 }, { duration: 800, easing: tween.elasticOut, onFinish: function() { // Change title to show wave number title.setText('WAVE ' + nextBossWave + ' BOSS'); title.fill = 0xffff00; // Change to yellow // Add the boss name text below the chip var nameText = new Text2(selectedChip.bossType.name.toUpperCase(), { size: 100, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); nameText.anchor.set(0.5, 0.5); nameText.y = 200; nameText.alpha = 0; selectedChip.addChild(nameText); // Add description text var descText = new Text2(selectedChip.bossType.description, { size: 60, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); descText.anchor.set(0.5, 0.5); descText.y = 320; descText.alpha = 0; selectedChip.addChild(descText); // Fade in the texts tween(nameText, { alpha: 1 }, { duration: 500 }); tween(descText, { alpha: 1 }, { duration: 500, delay: 200, onFinish: function() { // Store the selected boss self.selectedBosses[nextBossWave] = finalBossType; self.currentBossType = finalBossType; // Wait then fade out and continue LK.setTimeout(function() { tween(self.rouletteContainer, { alpha: 0 }, { duration: 500, onFinish: function() { if (self.rouletteContainer && self.rouletteContainer.parent) { self.rouletteContainer.parent.removeChild(self.rouletteContainer); } self.rouletteContainer = null; callback(); } }); }, 2000); } }); } }); } } }); }, // Remove or comment out the showBossSelection function since it's now integrated /* showBossSelection: function showBossSelection(bossType, waveNumber, callback) { // This function is no longer needed } */
User prompt
Update as needed with: showBossRoulette: function showBossRoulette(callback) { var nextBossWave = this.getNextBossWave(); this.rouletteContainer = new Container(); uiLayer.addChild(this.rouletteContainer); // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.8; this.rouletteContainer.addChild(bg); // Title var title = new Text2('BOSS INCOMING', { size: 120, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 400; this.rouletteContainer.addChild(title); // Horizontal scrolling container var scrollContainer = new Container(); scrollContainer.x = 0; scrollContainer.y = SCREEN_HEIGHT / 2; this.rouletteContainer.addChild(scrollContainer); // Create a long row of boss chips var chipSpacing = 300; var totalChips = 50; var bossChips = []; // Determine which boss will be selected var finalBossType = this.selectRandomBossType(); var finalIndex = this.bossTypes.indexOf(finalBossType); // Calculate positioning var centerX = SCREEN_WIDTH / 2; var finalChipIndex = Math.floor(totalChips * 0.7); // Create the scrolling chips for (var i = 0; i < totalChips; i++) { var chipContainer = new Container(); chipContainer.x = i * chipSpacing; chipContainer.y = 0; scrollContainer.addChild(chipContainer); // Determine which boss type this chip represents var bossTypeIndex; if (i === finalChipIndex) { bossTypeIndex = finalIndex; } else { bossTypeIndex = Math.floor(Math.random() * this.bossTypes.length); } var bossType = this.bossTypes[bossTypeIndex]; // Boss chip graphic var chip = LK.getAsset(bossType.chipAsset, { anchorX: 0.5, anchorY: 0.5 }); chip.scale.set(2); chipContainer.addChild(chip); // Store reference to the final chip if (i === finalChipIndex) { chipContainer.isFinalChip = true; chipContainer.bossType = bossType; } bossChips.push(chipContainer); } // Start the container off-screen to the left scrollContainer.x = -SCREEN_WIDTH; // Calculate final position so the selected chip ends up in center var finalScrollX = centerX - (finalChipIndex * chipSpacing); // Animate the scrolling var scrollDuration = 3000; var self = this; tween(scrollContainer, { x: finalScrollX }, { duration: scrollDuration, easing: tween.quadOut, onFinish: function() { // Find the final chip var selectedChip = null; for (var i = 0; i < bossChips.length; i++) { if (bossChips[i].isFinalChip) { selectedChip = bossChips[i]; break; } } if (selectedChip) { // Fade out all other chips for (var i = 0; i < bossChips.length; i++) { if (!bossChips[i].isFinalChip) { tween(bossChips[i], { alpha: 0 }, { duration: 500 }); } } // Scale up and highlight the selected chip var selectedBossChip = selectedChip.children[0]; selectedBossChip.tint = 0xffff00; tween(selectedChip, { scaleX: 2.0, // Scale up more dramatically scaleY: 2.0 }, { duration: 800, easing: tween.elasticOut, onFinish: function() { // Change title to show wave number title.setText('WAVE ' + nextBossWave + ' BOSS'); title.fill = 0xffff00; // Change to yellow // Add the boss name text below the chip var nameText = new Text2(selectedChip.bossType.name.toUpperCase(), { size: 100, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); nameText.anchor.set(0.5, 0.5); nameText.y = 200; nameText.alpha = 0; selectedChip.addChild(nameText); // Add description text var descText = new Text2(selectedChip.bossType.description, { size: 60, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); descText.anchor.set(0.5, 0.5); descText.y = 320; descText.alpha = 0; selectedChip.addChild(descText); // Fade in the texts tween(nameText, { alpha: 1 }, { duration: 500 }); tween(descText, { alpha: 1 }, { duration: 500, delay: 200, onFinish: function() { // Store the selected boss self.selectedBosses[nextBossWave] = finalBossType; self.currentBossType = finalBossType; // Wait then fade out and continue LK.setTimeout(function() { tween(self.rouletteContainer, { alpha: 0 }, { duration: 500, onFinish: function() { if (self.rouletteContainer && self.rouletteContainer.parent) { self.rouletteContainer.parent.removeChild(self.rouletteContainer); } self.rouletteContainer = null; callback(); } }); }, 2000); } }); } }); } } }); },
User prompt
Update as needed with: showBossRoulette: function showBossRoulette(callback) { var nextBossWave = this.getNextBossWave(); this.rouletteContainer = new Container(); uiLayer.addChild(this.rouletteContainer); // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.8; this.rouletteContainer.addChild(bg); // Title var title = new Text2('BOSS INCOMING', { size: 120, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 400; this.rouletteContainer.addChild(title); // Horizontal scrolling container var scrollContainer = new Container(); scrollContainer.x = 0; scrollContainer.y = SCREEN_HEIGHT / 2; this.rouletteContainer.addChild(scrollContainer); // Create a long row of boss chips var chipSpacing = 300; var totalChips = 50; var bossChips = []; // Determine which boss will be selected var finalBossType = this.selectRandomBossType(); var finalIndex = this.bossTypes.indexOf(finalBossType); // Calculate positioning var centerX = SCREEN_WIDTH / 2; var finalChipIndex = Math.floor(totalChips * 0.7); // Create the scrolling chips for (var i = 0; i < totalChips; i++) { var chipContainer = new Container(); chipContainer.x = i * chipSpacing; chipContainer.y = 0; scrollContainer.addChild(chipContainer); // Determine which boss type this chip represents var bossTypeIndex; if (i === finalChipIndex) { bossTypeIndex = finalIndex; } else { bossTypeIndex = Math.floor(Math.random() * this.bossTypes.length); } var bossType = this.bossTypes[bossTypeIndex]; // Boss chip graphic var chip = LK.getAsset(bossType.chipAsset, { anchorX: 0.5, anchorY: 0.5 }); chip.scale.set(2); chipContainer.addChild(chip); // Store reference to the final chip if (i === finalChipIndex) { chipContainer.isFinalChip = true; chipContainer.bossType = bossType; } bossChips.push(chipContainer); } // Start the container off-screen to the left scrollContainer.x = -SCREEN_WIDTH; // Calculate final position so the selected chip ends up in center var finalScrollX = centerX - (finalChipIndex * chipSpacing); // Animate the scrolling var scrollDuration = 3000; var self = this; tween(scrollContainer, { x: finalScrollX }, { duration: scrollDuration, easing: tween.quadOut, onFinish: function() { // Find the final chip var selectedChip = null; for (var i = 0; i < bossChips.length; i++) { if (bossChips[i].isFinalChip) { selectedChip = bossChips[i]; break; } } if (selectedChip) { // Fade out all other chips for (var i = 0; i < bossChips.length; i++) { if (!bossChips[i].isFinalChip) { tween(bossChips[i], { alpha: 0 }, { duration: 500 }); } } // Scale up and highlight the selected chip var selectedBossChip = selectedChip.children[0]; selectedBossChip.tint = 0xffff00; tween(selectedChip, { scaleX: 2.0, // Scale up more dramatically scaleY: 2.0 }, { duration: 800, easing: tween.elasticOut, onFinish: function() { // Change title to show wave number title.setText('WAVE ' + nextBossWave + ' BOSS'); title.fill = 0xffff00; // Change to yellow // Add the boss name text below the chip var nameText = new Text2(selectedChip.bossType.name.toUpperCase(), { size: 100, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); nameText.anchor.set(0.5, 0.5); nameText.y = 200; nameText.alpha = 0; selectedChip.addChild(nameText); // Add description text var descText = new Text2(selectedChip.bossType.description, { size: 60, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); descText.anchor.set(0.5, 0.5); descText.y = 320; descText.alpha = 0; selectedChip.addChild(descText); // Fade in the texts tween(nameText, { alpha: 1 }, { duration: 500 }); tween(descText, { alpha: 1 }, { duration: 500, delay: 200, onFinish: function() { // Store the selected boss self.selectedBosses[nextBossWave] = finalBossType; self.currentBossType = finalBossType; // Wait then fade out and continue LK.setTimeout(function() { tween(self.rouletteContainer, { alpha: 0 }, { duration: 500, onFinish: function() { if (self.rouletteContainer && self.rouletteContainer.parent) { self.rouletteContainer.parent.removeChild(self.rouletteContainer); } self.rouletteContainer = null; callback(); } }); }, 2000); } }); } }); } } }); }, // Remove or comment out the showBossSelection function since it's now integrated /* showBossSelection: function showBossSelection(bossType, waveNumber, callback) { // This function is no longer needed } */
User prompt
Update with: showBossRoulette: function showBossRoulette(callback) { var nextBossWave = this.getNextBossWave(); this.rouletteContainer = new Container(); uiLayer.addChild(this.rouletteContainer); // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.8; this.rouletteContainer.addChild(bg); // Title var title = new Text2('BOSS INCOMING', { size: 120, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 400; this.rouletteContainer.addChild(title); // Horizontal scrolling container var scrollContainer = new Container(); scrollContainer.x = 0; scrollContainer.y = SCREEN_HEIGHT / 2; this.rouletteContainer.addChild(scrollContainer); // Create a long row of boss chips var chipSpacing = 300; var totalChips = 50; var bossChips = []; // Determine which boss will be selected var finalBossType = this.selectRandomBossType(); var finalIndex = this.bossTypes.indexOf(finalBossType); // Calculate positioning var centerX = SCREEN_WIDTH / 2; var finalChipIndex = Math.floor(totalChips * 0.7); // Create the scrolling chips for (var i = 0; i < totalChips; i++) { var chipContainer = new Container(); chipContainer.x = i * chipSpacing; chipContainer.y = 0; scrollContainer.addChild(chipContainer); // Determine which boss type this chip represents var bossTypeIndex; if (i === finalChipIndex) { bossTypeIndex = finalIndex; } else { bossTypeIndex = Math.floor(Math.random() * this.bossTypes.length); } var bossType = this.bossTypes[bossTypeIndex]; // Boss chip graphic var chip = LK.getAsset(bossType.chipAsset, { anchorX: 0.5, anchorY: 0.5 }); chip.scale.set(2); chipContainer.addChild(chip); // Store reference to the final chip if (i === finalChipIndex) { chipContainer.isFinalChip = true; chipContainer.bossType = bossType; } bossChips.push(chipContainer); } // Start the container off-screen to the left scrollContainer.x = -SCREEN_WIDTH; // Calculate final position so the selected chip ends up in center var finalScrollX = centerX - (finalChipIndex * chipSpacing); // Animate the scrolling var scrollDuration = 3000; var self = this; tween(scrollContainer, { x: finalScrollX }, { duration: scrollDuration, easing: tween.quadOut, onFinish: function() { // Find the final chip and verify it's centered var selectedChip = null; for (var i = 0; i < bossChips.length; i++) { if (bossChips[i].isFinalChip) { selectedChip = bossChips[i]; break; } } if (selectedChip) { // Check the actual position - should be at centerX var actualCenterX = scrollContainer.x + selectedChip.x; console.log("Selected chip should be at:", centerX, "Actually at:", actualCenterX); // Fade out all other chips for (var i = 0; i < bossChips.length; i++) { if (!bossChips[i].isFinalChip) { tween(bossChips[i], { alpha: 0 }, { duration: 500 }); } } // Scale up and highlight the selected chip var selectedBossChip = selectedChip.children[0]; selectedBossChip.tint = 0xffff00; tween(selectedChip, { scaleX: 1.5, scaleY: 1.5 }, { duration: 800, easing: tween.elasticOut, onFinish: function() { // Add the boss name text var nameText = new Text2(selectedChip.bossType.name.toUpperCase(), { size: 80, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); nameText.anchor.set(0.5, 0.5); nameText.y = 200; nameText.alpha = 0; selectedChip.addChild(nameText); // Fade in the name tween(nameText, { alpha: 1 }, { duration: 500, onFinish: function() { self.selectedBosses[nextBossWave] = finalBossType; self.currentBossType = finalBossType; LK.setTimeout(function() { self.showBossSelection(finalBossType, nextBossWave, callback); }, 1000); } }); } }); } } }); }
User prompt
Update as needed with: showBossRoulette: function showBossRoulette(callback) { var nextBossWave = this.getNextBossWave(); this.rouletteContainer = new Container(); uiLayer.addChild(this.rouletteContainer); // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.8; this.rouletteContainer.addChild(bg); // Title var title = new Text2('BOSS INCOMING', { size: 120, fill: 0xff0000, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 400; this.rouletteContainer.addChild(title); // Horizontal scrolling container var scrollContainer = new Container(); scrollContainer.x = 0; scrollContainer.y = SCREEN_HEIGHT / 2; this.rouletteContainer.addChild(scrollContainer); // Create a long row of boss chips var chipSpacing = 300; var totalChips = 50; // Create many chips for smooth scrolling effect var bossChips = []; // Determine which boss will be selected var finalBossType = this.selectRandomBossType(); var finalIndex = this.bossTypes.indexOf(finalBossType); // Calculate where the final boss should stop (center of screen) var centerX = SCREEN_WIDTH / 2; var finalChipIndex = Math.floor(totalChips * 0.7); // Stop at 70% through the sequence var finalStopX = centerX - (finalChipIndex * chipSpacing); // Create the scrolling chips for (var i = 0; i < totalChips; i++) { var chipContainer = new Container(); chipContainer.x = i * chipSpacing - SCREEN_WIDTH; // Start off-screen to the left chipContainer.y = 0; scrollContainer.addChild(chipContainer); // Determine which boss type this chip represents var bossTypeIndex; if (i === finalChipIndex) { // This is the chip that will be selected bossTypeIndex = finalIndex; } else { // Random boss type for other positions bossTypeIndex = Math.floor(Math.random() * this.bossTypes.length); } var bossType = this.bossTypes[bossTypeIndex]; // Boss chip graphic var chip = LK.getAsset(bossType.chipAsset, { anchorX: 0.5, anchorY: 0.5 }); chip.scale.set(2); chipContainer.addChild(chip); // Store reference to the final chip if (i === finalChipIndex) { chipContainer.isFinalChip = true; chipContainer.bossType = bossType; } bossChips.push(chipContainer); } // Animate the scrolling var scrollDuration = 3000; var self = this; tween(scrollContainer, { x: finalStopX }, { duration: scrollDuration, easing: tween.quadOut, onFinish: function() { // Find the final chip var selectedChip = null; for (var i = 0; i < bossChips.length; i++) { if (bossChips[i].isFinalChip) { selectedChip = bossChips[i]; break; } } if (selectedChip) { // Fade out all other chips for (var i = 0; i < bossChips.length; i++) { if (!bossChips[i].isFinalChip) { tween(bossChips[i], { alpha: 0 }, { duration: 500 }); } } // Scale up and highlight the selected chip var selectedBossChip = selectedChip.children[0]; // The actual chip graphic selectedBossChip.tint = 0xffff00; // Yellow highlight tween(selectedChip, { scaleX: 1.5, scaleY: 1.5 }, { duration: 800, easing: tween.elasticOut, onFinish: function() { // Add the boss name text var nameText = new Text2(selectedChip.bossType.name.toUpperCase(), { size: 80, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); nameText.anchor.set(0.5, 0.5); nameText.y = 200; nameText.alpha = 0; selectedChip.addChild(nameText); // Fade in the name tween(nameText, { alpha: 1 }, { duration: 500, onFinish: function() { // Store the selected boss for the future wave self.selectedBosses[nextBossWave] = finalBossType; self.currentBossType = finalBossType; // Wait a moment then show the selection screen LK.setTimeout(function() { self.showBossSelection(finalBossType, nextBossWave, callback); }, 1000); } }); } }); } } }); }
User prompt
Update as needed with: self.activate = function (value, isPlayerSide, startPos) { self.active = true; self.visible = true; self.value = value; self.isPlayerSide = isPlayerSide; // Mark boss chips (they have much higher values than normal chips) self.isBoss = value >= 100; // Set boss properties BEFORE calling setChipAppearance self.bossType = null; self.bossAbilityTimer = 0; self.bossImmuneToSuit = null; self.bossImmuneTimer = 0; self.bossScrambleCount = 0; self.bossRainTimer = 0; self.bossLockedCard = null; self.bossLockGraphic = null; self.bossSpeedStartTime = 0; self.bossOriginalSpeed = 0; // Set boss type if this is a PvP boss if (self.isBoss && typeof gameMode !== 'undefined' && gameMode === 'pvp' && typeof BossSystem !== 'undefined' && BossSystem.currentBossType) { self.bossType = BossSystem.currentBossType.id; self.bossAbilityTimer = 0; self.bossSpeedStartTime = LK.ticks; self.bossOriginalSpeed = self.speed; // Speed boss starts slower if (self.bossType === 'speed') { self.speed = self.speed * 0.5; // Start at 50% speed } // Immune boss starts with random suit immunity if (self.bossType === 'immune') { var suits = ['hearts', 'diamonds', 'clubs', 'spades']; self.bossImmuneToSuit = suits[Math.floor(Math.random() * suits.length)]; self.bossImmuneTimer = 0; } // Lock boss locks a random card if (self.bossType === 'lock') { if (typeof self.lockRandomCard === 'function') { self.lockRandomCard(); } } } // Health calculations and other setup... // (keep all the existing health calculation code here) // Reset other properties... // (keep all the existing reset code here) // NOW call setChipAppearance after boss properties are set self.setChipAppearance(); self.x = startPos.x; self.y = startPos.y; };
User prompt
Update as needed with: self.setChipAppearance = function () { // Hide previous graphic and reset its scale to default if it exists if (chipGraphics) { chipGraphics.visible = false; chipGraphics.scale.set(1); } // For boss chips in PvP mode, use the specific boss asset if (self.isBoss && typeof gameMode !== 'undefined' && gameMode === 'pvp' && self.bossType) { // Find the boss type data to get the correct asset var bossTypeData = BossSystem.bossTypes.find(function(type) { return type.id === self.bossType; }); if (bossTypeData) { // Create the boss chip graphic if it doesn't exist in our assets collection if (!chipGraphicsAssets[bossTypeData.chipAsset]) { chipGraphicsAssets[bossTypeData.chipAsset] = self.attachAsset(bossTypeData.chipAsset, { anchorX: 0.5, anchorY: 0.5 }); chipGraphicsAssets[bossTypeData.chipAsset].visible = false; } chipGraphics = chipGraphicsAssets[bossTypeData.chipAsset]; } else { // Fallback to regular chip graphics chipGraphics = chipGraphicsAssets[self.value] || chipGraphicsAssets[1]; } } else { // Regular chip logic chipGraphics = chipGraphicsAssets[self.value] || chipGraphicsAssets[1]; } if (chipGraphics) { chipGraphics.visible = true; // Apply boss scaling if (self.isBoss) { chipGraphics.scale.set(1.25); } } self.updateHealthText(); };
Code edit (1 edits merged)
Please save this source code
User prompt
Update as needed with: // In PokerChip.activate, modify the boss ability setup: if (isBoss && gameMode === 'pvp' && BossSystem.currentBossType) { self.bossType = BossSystem.currentBossType.id; // ... rest of boss ability initialization remains the same ... }
User prompt
Update as needed with: // NEW: Enhanced particle effect for chip explosions - celebratory version function createChipExplosionParticles(x, y, chipValue) { var numParticles = Math.min(20, 10 + chipValue); // More particles for celebration var chipColor = getChipColor(chipValue); for (var i = 0; i < numParticles; i++) { var particle = LK.getAsset('explosionParticle', { anchorX: 0.5, anchorY: 0.5 }); particle.x = x + (Math.random() - 0.5) * 40; // Start in a tight cluster particle.y = y + (Math.random() - 0.5) * 40; particle.tint = chipColor; particle.scale.set(0.4 + Math.random() * 0.8); // Launch particles upward in a celebratory fountain pattern var angle = -Math.PI/2 + (Math.random() - 0.5) * Math.PI * 0.8; // Mostly upward, some spread var initialSpeed = 300 + Math.random() * 400; // Strong upward velocity var gravity = 600; // Gravity pulls them down var velocityX = Math.cos(angle) * initialSpeed; var velocityY = Math.sin(angle) * initialSpeed; // Negative Y is upward var duration = 2000 + Math.random() * 1000; // 2-3 seconds of flight time var targetX = x + velocityX * (duration / 1000); var targetY = y + velocityY * (duration / 1000) + 0.5 * gravity * Math.pow(duration / 1000, 2); // Ensure particles fall well off screen for full celebration effect if (targetY < SCREEN_HEIGHT + 200) { targetY = SCREEN_HEIGHT + 200 + Math.random() * 300; } gameLayer.addChild(particle); // Animate with celebratory spinning and fading tween(particle, { x: targetX, y: targetY, alpha: 0, rotation: Math.random() * Math.PI * 6 // Extra spinning for celebration }, { duration: duration, easing: tween.quadIn, // Natural gravity acceleration onFinish: function(p) { return function() { if (p.parent) { p.parent.removeChild(p); } }; }(particle) }); } }
User prompt
Update as needed with: // NEW: Enhanced particle effect for chip explosions - celebratory version function createChipExplosionParticles(x, y, chipValue) { var numParticles = Math.min(20, 10 + chipValue); // More particles for celebration var chipColor = getChipColor(chipValue); for (var i = 0; i < numParticles; i++) { var particle = LK.getAsset('explosionParticle', { anchorX: 0.5, anchorY: 0.5 }); particle.x = x + (Math.random() - 0.5) * 40; // Start in a tight cluster particle.y = y + (Math.random() - 0.5) * 40; particle.tint = chipColor; particle.scale.set(0.4 + Math.random() * 0.8); // Launch particles upward in a celebratory fountain pattern var angle = -Math.PI/2 + (Math.random() - 0.5) * Math.PI * 0.8; // Mostly upward, some spread var initialSpeed = 300 + Math.random() * 400; // Strong upward velocity var gravity = 600; // Gravity pulls them down var velocityX = Math.cos(angle) * initialSpeed; var velocityY = Math.sin(angle) * initialSpeed; // Negative Y is upward var duration = 2000 + Math.random() * 1000; // 2-3 seconds of flight time var targetX = x + velocityX * (duration / 1000); var targetY = y + velocityY * (duration / 1000) + 0.5 * gravity * Math.pow(duration / 1000, 2); // Ensure particles fall well off screen for full celebration effect if (targetY < SCREEN_HEIGHT + 200) { targetY = SCREEN_HEIGHT + 200 + Math.random() * 300; } gameLayer.addChild(particle); // Animate with celebratory spinning and fading tween(particle, { x: targetX, y: targetY, alpha: 0, rotation: Math.random() * Math.PI * 6 // Extra spinning for celebration }, { duration: duration, easing: tween.quadIn, // Natural gravity acceleration onFinish: function(p) { return function() { if (p.parent) { p.parent.removeChild(p); } }; }(particle) }); } }
User prompt
Update as needed with: // NEW: Enhanced particle effect for chip explosions - celebratory version function createChipExplosionParticles(x, y, chipValue) { var numParticles = Math.min(20, 10 + chipValue); // More particles for celebration var chipColor = getChipColor(chipValue); for (var i = 0; i < numParticles; i++) { var particle = LK.getAsset('explosionParticle', { anchorX: 0.5, anchorY: 0.5 }); particle.x = x + (Math.random() - 0.5) * 40; // Start in a tight cluster particle.y = y + (Math.random() - 0.5) * 40; particle.tint = chipColor; particle.scale.set(0.4 + Math.random() * 0.8); // Launch particles upward in a celebratory fountain pattern var angle = -Math.PI/2 + (Math.random() - 0.5) * Math.PI * 0.8; // Mostly upward, some spread var initialSpeed = 300 + Math.random() * 400; // Strong upward velocity var gravity = 600; // Gravity pulls them down var velocityX = Math.cos(angle) * initialSpeed; var velocityY = Math.sin(angle) * initialSpeed; // Negative Y is upward var duration = 2000 + Math.random() * 1000; // 2-3 seconds of flight time var targetX = x + velocityX * (duration / 1000); var targetY = y + velocityY * (duration / 1000) + 0.5 * gravity * Math.pow(duration / 1000, 2); // Ensure particles fall well off screen for full celebration effect if (targetY < SCREEN_HEIGHT + 200) { targetY = SCREEN_HEIGHT + 200 + Math.random() * 300; } gameLayer.addChild(particle); // Animate with celebratory spinning and fading tween(particle, { x: targetX, y: targetY, alpha: 0, rotation: Math.random() * Math.PI * 6 // Extra spinning for celebration }, { duration: duration, easing: tween.quadIn, // Natural gravity acceleration onFinish: function(p) { return function() { if (p.parent) { p.parent.removeChild(p); } }; }(particle) }); } }
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { battleRating: 0, money: 0, ownedMods: {}, randomModPrice: 300, tutorialCompleted: false, coopHighestWave: 0 }); /**** * Classes ****/ /**** * Bullet Class ****/ var Bullet = Container.expand(function () { var self = Container.call(this); self.active = false; self.target = null; self.damage = 10; self.speed = 11.5; self.isPlayerCard = true; self.isSeekingLastPosition = false; self.targetLastX = 0; self.targetLastY = 0; self.level = 1; self.ricochetCount = 0; self.ricochetedFrom = []; var currentGraphic = null; // Add suitCircle backgrounds for each suit var suitCircleGraphics = { 'hearts': self.attachAsset('suitCircle', { anchorX: 0.5, anchorY: 0.5 }), 'diamonds': self.attachAsset('suitCircle', { anchorX: 0.5, anchorY: 0.5 }), 'clubs': self.attachAsset('suitCircle', { anchorX: 0.5, anchorY: 0.5 }), 'spades': self.attachAsset('suitCircle', { anchorX: 0.5, anchorY: 0.5 }) }; var baseSuitGraphics = { 'hearts': self.attachAsset('heartSuit', { anchorX: 0.5, anchorY: 0.5 }), 'diamonds': self.attachAsset('diamondSuit', { anchorX: 0.5, anchorY: 0.5 }), 'clubs': self.attachAsset('clubSuit', { anchorX: 0.5, anchorY: 0.5 }), 'spades': self.attachAsset('spadeSuit', { anchorX: 0.5, anchorY: 0.5 }) }; var modGraphicsCache = {}; // Set scale and hide all suit graphics and suit circles initially. for (var suit in baseSuitGraphics) { var graphic = baseSuitGraphics[suit]; var circle = suitCircleGraphics[suit]; graphic.scale.set(0.3); // Small bullets graphic.visible = false; if (circle) { circle.scale.set(0.45); // Slightly larger than suit icon circle.visible = false; // Ensure circle is rendered below the suit icon self.setChildIndex(circle, Math.max(0, self.getChildIndex(graphic) - 1)); } } self.activate = function (startX, startY, target, damage, suit, isPlayerCard, level) { self.active = true; self.visible = true; self.x = startX; self.y = startY; self.target = target; self.damage = damage; self.isPlayerCard = isPlayerCard; self.level = level || 1; self.suit = suit || 'hearts'; self.isSeekingLastPosition = false; self.ricochetCount = 0; self.ricochetedFrom = []; self.hasRetargeted = false; if (suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', isPlayerCard) === 'ricochetClubsMod') { // 2 bounces, +1 every 2 levels. Max 5. self.ricochetCount = Math.min(5, 2 + Math.floor((self.level - 1) / 2)); } if (self.target) { self.targetLastX = self.target.x; self.targetLastY = self.target.y; } // Hide previous suitCircle if any if (currentGraphic) { currentGraphic.visible = false; // Hide all suit circles for (var suitKey in suitCircleGraphics) { if (suitCircleGraphics[suitKey]) { suitCircleGraphics[suitKey].visible = false; } } } suit = suit || 'hearts'; var equippedMod = ModSystem.getEquippedModAsset(suit, self.isPlayerCard); if (equippedMod) { if (!modGraphicsCache[equippedMod]) { modGraphicsCache[equippedMod] = self.attachAsset(equippedMod, { anchorX: 0.5, anchorY: 0.5 }); modGraphicsCache[equippedMod].scale.set(0.3); modGraphicsCache[equippedMod].visible = false; } currentGraphic = modGraphicsCache[equippedMod]; } else { currentGraphic = baseSuitGraphics[suit]; } currentGraphic.visible = true; // Show and position the suitCircle under the current suit asset if (suitCircleGraphics[suit]) { suitCircleGraphics[suit].visible = true; // Ensure suitCircle is rendered below the suit icon if (self.getChildIndex(suitCircleGraphics[suit]) > self.getChildIndex(currentGraphic)) { self.setChildIndex(suitCircleGraphics[suit], Math.max(0, self.getChildIndex(currentGraphic) - 1)); } // Center the circle (should already be, but for safety) suitCircleGraphics[suit].x = 0; suitCircleGraphics[suit].y = 0; } }; self.findNewTarget = function () { // Use the same targeting logic as cards, but from bullet's perspective var targets; if (gameMode === 'coop') { targets = activePlayerChips.concat(activeAIChips); } else { targets = self.isPlayerCard ? activePlayerChips : activeAIChips; } var bestTarget = null; var shortestDistanceSq = Infinity; var turningPointY = SCREEN_HEIGHT / 2 - 200; for (var i = 0; i < targets.length; i++) { var chip = targets[i]; if (chip.active) { if (gameMode === 'coop') { var isPlayerChip = chip.isPlayerSide; var isPlayerBullet = self.isPlayerCard; var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY; if (!onHorizontalPath) { if (isPlayerBullet !== isPlayerChip) { continue; } } } var dx = chip.x - self.x; var dy = chip.y - self.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < shortestDistanceSq) { shortestDistanceSq = distanceSq; bestTarget = chip; } } } return bestTarget; }; self.findRicochetTarget = function () { // Use the same targeting logic as cards, but from bullet's perspective var targets; if (gameMode === 'coop') { targets = activePlayerChips.concat(activeAIChips); } else { targets = self.isPlayerCard ? activePlayerChips : activeAIChips; } var bestTarget = null; var shortestDistanceSq = Infinity; var turningPointY = SCREEN_HEIGHT / 2 - 200; for (var i = 0; i < targets.length; i++) { var potentialTarget = targets[i]; if (potentialTarget.active && self.ricochetedFrom.indexOf(potentialTarget) === -1) { if (gameMode === 'coop') { var isPlayerChip = potentialTarget.isPlayerSide; var isPlayerBullet = self.isPlayerCard; var onHorizontalPath = isPlayerChip ? potentialTarget.y <= turningPointY : potentialTarget.y >= turningPointY; if (!onHorizontalPath) { if (isPlayerBullet !== isPlayerChip) { continue; } } } var dx = potentialTarget.x - self.x; var dy = potentialTarget.y - self.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < shortestDistanceSq) { shortestDistanceSq = distanceSq; bestTarget = potentialTarget; } } } return bestTarget; }; self.update = function () { if (!self.active) { return; } // If target is destroyed, try to find a new target instead of seeking last position if (self.target && !self.target.active) { var newTarget = null; if (!self.hasRetargeted) { newTarget = self.findNewTarget(); } if (newTarget) { self.target = newTarget; self.targetLastX = newTarget.x; self.targetLastY = newTarget.y; self.hasRetargeted = true; } else { // No new target available, or already retargeted. Seek last position. self.isSeekingLastPosition = true; self.target = null; } } // If seeking, move towards the last position if (self.isSeekingLastPosition) { var dx = self.targetLastX - self.x; var dy = self.targetLastY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < self.speed) { // Arrived at destination, recycle bullet self.isSeekingLastPosition = false; if (currentGraphic) { currentGraphic.visible = false; } PoolManager.returnBullet(self); } else { // Move towards destination var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } } else if (self.target) { // Store previous position for continuous collision detection var prevX = self.x; var prevY = self.y; // Update last known position self.targetLastX = self.target.x; self.targetLastY = self.target.y; var dx = self.target.x - self.x; var dy = self.target.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Calculate where bullet will be after this frame var angle = Math.atan2(dy, dx); var newX = self.x + Math.cos(angle) * self.speed; var newY = self.y + Math.sin(angle) * self.speed; // RELIABLE COLLISION DETECTION: // Check if the bullet's path for this frame intersects the target's radius. // This prevents "tunneling" where a fast bullet could pass through a target between frames. var hitRadius = 85; // Visual radius of the chip, matches chip graphics var collisionDistance = distancePointToLine(self.target.x, self.target.y, prevX, prevY, newX, newY); if (collisionDistance <= hitRadius) { // Hit detected // NEW: Check for Execute mod for Spades BEFORE dealing damage if (self.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'deathSpadesMod') { // Execute threshold: starts at 15%, +1% per level. Max 40%. var executeThreshold = Math.min(0.40, 0.15 + (self.level - 1) * 0.01); // Execute chance: starts at 20%, +2% per level. Max 50%. var executeChance = Math.min(0.50, 0.20 + (self.level - 1) * 0.02); if (self.target.health / self.target.maxHealth <= executeThreshold) { if (Math.random() < executeChance) { // Execute! createFloatingText('EXECUTE!', self.target.x, self.target.y - 60, 0xaa00aa, 50); // Create the execution animation var executionEffect = LK.getAsset('deathSpadesMod', { anchorX: 0.5, anchorY: 0.5, x: self.target.x, y: self.target.y, scaleX: 0.1, scaleY: 0.1, alpha: 1 }); gameLayer.addChild(executionEffect); tween(executionEffect, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 500, easing: tween.quadOut, onFinish: function onFinish() { if (executionEffect.parent) { executionEffect.parent.removeChild(executionEffect); } } }); self.target.takeDamage(self.target.health, self.isPlayerCard); // Deal fatal damage // Recycle bullet and stop further processing if (currentGraphic) { currentGraphic.visible = false; } PoolManager.returnBullet(self); return; // IMPORTANT: Exit update for this bullet } } } // If not executed, proceed with normal hit logic self.target.takeDamage(self.damage, self.isPlayerCard); // NEW: Check if this is a hearts bullet and flame mod is equipped if (self.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', self.isPlayerCard) === 'burnHeartMod') { // Calculate burn damage (10% of hit damage per tick) var burnDamage = self.damage * 0.1; // Increase burn duration by 50% (from 5 to 7.5 ticks) var burnDuration = Math.round(5 * 1.5); // 7.5 → 8 ticks = ~4 seconds of burning // Apply burn effect self.target.applyBurn(burnDamage, burnDuration, self.isPlayerCard); } // NEW: Check if this is a spades bullet and freeze mod is equipped if (self.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'freezeSpadeMod') { // Calculate freeze duration (3 seconds base) var freezeDuration = 180; // 180 ticks at 60fps = 3 seconds // Apply freeze effect self.target.applyFreeze(freezeDuration); } // NEW: Check if this is a clubs bullet and slow mod is equipped if (self.suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', self.isPlayerCard) === 'slowClubsMod') { // Slow amount: 20% base, +5% per level. Max 70%. var slowAmount = Math.min(0.7, 0.2 + (self.level - 1) * 0.05); // Slow duration: 2s base, +0.5s per level. var slowDuration = 120 + (self.level - 1) * 30; // 120 ticks = 2s self.target.applySlow(slowAmount, slowDuration); } // NEW: Check for ricochet mod for clubs if (self.ricochetCount > 0) { self.ricochetedFrom.push(self.target); var newTarget = self.findRicochetTarget(); if (newTarget) { self.target = newTarget; self.targetLastX = newTarget.x; self.targetLastY = newTarget.y; self.ricochetCount--; self.damage = Math.floor(self.damage * 0.7); // 70% damage on next hit // Visual feedback for ricochet createExplosion(self.x, self.y, 0xcccccc); return; // Keep the bullet alive and moving to the new target } } if (currentGraphic) { currentGraphic.visible = false; } PoolManager.returnBullet(self); } else { // No hit, move bullet forward self.x = newX; self.y = newY; } } else { // No target and not seeking, recycle if (currentGraphic) { currentGraphic.visible = false; } PoolManager.returnBullet(self); } }; return self; }); // Helper function to calculate distance from point to line segment /**** * Object Pool Manager ****/ /**** * Card Class ****/ var Card = Container.expand(function (cardData) { var self = Container.call(this); self.cardData = cardData; self.level = 1; self.isInPlay = false; self.isPlayerCard = true; // Track which player owns this card self.playSlotX = 0; self.playSlotY = 0; self.lastFired = 0; self.fireRate = 60; // Base fire rate (ticks between shots) self.damage = 35; // Increased base damage self.range = 200; self.handBonus = 1; self.timeOnBoard = 0; self.lastIncomeTick = 0; var redOutline = self.attachAsset('card', { anchorX: 0.5, anchorY: 0.5 }); redOutline.scale.set(1.1); redOutline.alpha = 1.0; redOutline.tint = 0xff0000; // Red color redOutline.visible = false; self.redOutline = redOutline; var greenOutline = self.attachAsset('card', { anchorX: 0.5, anchorY: 0.5 }); greenOutline.scale.set(1.1); greenOutline.alpha = 0.7; greenOutline.tint = 0x00ff00; greenOutline.visible = false; self.greenOutline = greenOutline; var cardGraphics = self.attachAsset('card', { anchorX: 0.5, anchorY: 0.5 }); // Define getSuitSymbol method before using it self.getSuitSymbol = function (suit) { switch (suit) { case 'hearts': return '♥'; case 'diamonds': return '♦'; case 'clubs': return '♣'; case 'spades': return '♠'; case 'joker': return '🃏'; default: return '?'; } }; // Card value in top left corner if (cardData.suit !== 'joker') { var valueText = new Text2(cardData.value, { size: 56, fill: CardSystem.suitColors[cardData.suit] || 0x000000, weight: 800, stroke: 0x000000, strokeThickness: 0 }); valueText.anchor.set(0, 0); valueText.x = -95; // Top left valueText.y = -135; self.addChild(valueText); } // Large suit symbol in center, deferred to activate() var suitGraphics = null; var jokerSuitGraphics = null; // Level text at bottom var levelText = new Text2('Lvl 1', { size: 45, fill: 0x000000, weight: 800, stroke: 0x000000, strokeThickness: 0 }); levelText.anchor.set(0.5, 1); levelText.y = 128; // Bottom of card self.addChild(levelText); self.activate = function (x, y, inPlay, isPlayerCard) { self.x = x; self.y = y; self.isInPlay = inPlay || false; self.isPlayerCard = isPlayerCard !== undefined ? isPlayerCard : true; self.visible = true; // Remove old graphics if they exist if (suitGraphics && suitGraphics.parent) { suitGraphics.parent.removeChild(suitGraphics); suitGraphics = null; } if (jokerSuitGraphics && jokerSuitGraphics.parent) { jokerSuitGraphics.parent.removeChild(jokerSuitGraphics); jokerSuitGraphics = null; } // Create new suit graphic based on player/AI context var suitAssetId = null; var equippedMod = ModSystem.getEquippedModAsset(self.cardData.suit, self.isPlayerCard); if (equippedMod) { suitAssetId = equippedMod; } else { switch (self.cardData.suit) { case 'hearts': suitAssetId = 'heartSuit'; break; case 'diamonds': suitAssetId = 'diamondSuit'; break; case 'clubs': suitAssetId = 'clubSuit'; break; case 'spades': suitAssetId = 'spadeSuit'; break; } } if (suitAssetId) { suitGraphics = self.attachAsset(suitAssetId, { anchorX: 0.5, anchorY: 0.5 }); suitGraphics.y = -15; suitGraphics.scaleX = suitGraphics.scaleY = 0.8; } else if (self.cardData.suit === 'joker') { jokerSuitGraphics = self.attachAsset('jokerSuit', { anchorX: 0.5, anchorY: 0.5 }); jokerSuitGraphics.y = -15; jokerSuitGraphics.scale.set(1.5); } if (inPlay) { self.calculateStats(); } }; self.calculateStats = function () { // Stats based on level only, not card face value var baseDamage = 10; // Reduced base damage var baseFireRate = 60; // Level scaling - more dramatic improvements per level self.damage = Math.floor(baseDamage * Math.pow(1.6, self.level - 1)); // Further decreased scaling from 1.7 to 1.6 self.fireRate = Math.max(15, Math.floor(baseFireRate / Math.pow(1.2, self.level - 1))); // Reduced from 1.25 to 1.2 // Apply poker hand bonus self.damage = Math.floor(self.damage * self.handBonus); self.fireRate = Math.max(10, Math.floor(self.fireRate / self.handBonus)); // NEW: Apply mod-specific stat changes if (self.cardData.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'mineSpadesMod') { // Mine mod significantly slows fire rate in exchange for higher damage per mine. self.fireRate *= 2.5; } }; self.setLevel = function (newLevel) { if (self.cardData.suit === 'joker') { self.level = 1; levelText.visible = false; self.calculateStats(); return; } self.level = newLevel; levelText.setText('Lvl ' + self.level); self.calculateStats(); self.resetInvestment(); // Visual feedback for higher levels if (self.level > 1) { // The glow effect was causing cards to become translucent. // Level up is already indicated by animation and floating text. } }; self.resetInvestment = function () { self.timeOnBoard = 0; self.lastIncomeTick = LK.ticks; }; self.canMergeWith = function (otherCard) { if (!otherCard || otherCard === self) { return false; } // If the card being dropped onto is a Joker, it cannot be leveled up. if (otherCard.cardData && otherCard.cardData.suit === 'joker') { return false; } // If the card being dragged is a Joker, it can merge with any non-Joker card. if (self.cardData.suit === 'joker') { return true; } // Must be same level AND (same suit OR same value) var sameLevel = self.level === otherCard.level; var sameSuit = self.cardData.suit === otherCard.cardData.suit; var sameValue = self.cardData.value === otherCard.cardData.value; return sameLevel && (sameSuit || sameValue); }; self.mergeWith = function (otherCard) { if (!self.canMergeWith(otherCard)) { return null; } // When merging, the new card levels up. The new level is one higher than the card on the board. var newLevel = otherCard.level + 1; // Special case: If the card being dragged is a Joker, the target card just increases its level if (self.cardData.suit === 'joker') { var mergedCard = new Card(otherCard.cardData); mergedCard.setLevel(newLevel); return mergedCard; } // The new card is of a random type, regardless of what was merged. var randomSuit = CardSystem.suits[Math.floor(Math.random() * CardSystem.suits.length)]; var randomValue = CardSystem.values[Math.floor(Math.random() * CardSystem.values.length)]; var newCardData = { suit: randomSuit, value: randomValue, id: randomSuit + '_' + randomValue }; var mergedCard = new Card(newCardData); mergedCard.setLevel(newLevel); return mergedCard; }; self.findTarget = function () { // Player cards target enemies attacking the player (activePlayerChips) // AI cards target enemies attacking the AI (activeAIChips) var targets; if (gameMode === 'coop') { // In coop, start with all targets and filter based on position targets = activePlayerChips.concat(activeAIChips); } else { targets = self.isPlayerCard ? activePlayerChips : activeAIChips; } var bestTarget = null; var highestProgress = -1; var turningPointY = SCREEN_HEIGHT / 2 - 200; for (var i = 0; i < targets.length; i++) { var chip = targets[i]; // Add extra validation for alive targets if (chip.active && chip.health > 0 && chip.visible && chip.pathProgress > highestProgress) { if (gameMode === 'coop') { var isPlayerChip = chip.isPlayerSide; var isPlayerCard = self.isPlayerCard; // Player chips move up (y decreases), AI chips move down (y increases). var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY; if (!onHorizontalPath) { // Not on the shared horizontal path yet, so cards can only target their own side's enemies. if (isPlayerCard !== isPlayerChip) { continue; } } // If onHorizontalPath is true, any card can target the chip, so no 'continue' needed. } highestProgress = chip.pathProgress; bestTarget = chip; } } return bestTarget; }; self.fire = function () { var target = self.findTarget(); if (!target) { return; } // NEW: Handle Mine Spades Mod if (self.cardData.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'mineSpadesMod') { var mine = PoolManager.getMine(self.isPlayerCard); if (mine) { var pathProgress = Math.random() * 70 + 15; var minePos = PathSystem.getPositionAlongPath(pathProgress, self.isPlayerCard); var mineDamage = Math.floor(self.damage * 2.5); // Mines deal 2.5x card damage mine.activate(minePos.x, minePos.y, mineDamage, self.isPlayerCard); gameLayer.addChild(mine); activeMines.push(mine); } } else { var damageToDeal = self.damage; // NEW: Calculate Unity Bonus for Hearts if (self.cardData.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', self.isPlayerCard) === 'unityHeartsMod') { var playArea = self.isPlayerCard ? gameState.playerPlayArea : gameState.aiPlayArea; var cardRow = -1, cardCol = -1; // Find card's position in the grid for (var r = 0; r < PLAY_AREA_ROWS; r++) { for (var c = 0; c < PLAY_AREA_COLS; c++) { if (playArea[r][c] === self) { cardRow = r; cardCol = c; break; } } if (cardRow !== -1) { break; } } if (cardRow !== -1) { var adjacentHearts = 0; var neighbors = [[cardRow - 1, cardCol], [cardRow + 1, cardCol], [cardRow, cardCol - 1], [cardRow, cardCol + 1]]; for (var i = 0; i < neighbors.length; i++) { var nRow = neighbors[i][0]; var nCol = neighbors[i][1]; if (nRow >= 0 && nRow < PLAY_AREA_ROWS && nCol >= 0 && nCol < PLAY_AREA_COLS) { var neighborCard = playArea[nRow][nCol]; if (neighborCard && neighborCard.cardData.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', neighborCard.isPlayerCard) === 'unityHeartsMod') { adjacentHearts++; } } } // Each adjacent heart adds 25% bonus damage damageToDeal = Math.floor(damageToDeal * (1 + 0.25 * adjacentHearts)); } } // NEW: Calculate Gambler Bonus for Diamonds if (self.cardData.suit === 'diamonds' && ModSystem.getEquippedModAsset('diamonds', self.isPlayerCard) === 'gamblerDiamondsMod') { // Base 50% chance of double damage, increases by 5% per level. Capped at 95%. var doubleDamageChance = Math.min(0.95, 0.5 + (self.level - 1) * 0.05); if (Math.random() < doubleDamageChance) { damageToDeal *= 2; createFloatingText('x2!', self.x, self.y - 70, 0xffd700, 30); } else { damageToDeal = 0; createFloatingText('Miss!', self.x, self.y - 70, 0xaaaaaa, 30); } } // NEW: Calculate Boost from adjacent Diamonds var playArea = self.isPlayerCard ? gameState.playerPlayArea : gameState.aiPlayArea; var cardRow = -1, cardCol = -1; // Find card's position in the grid for (var r = 0; r < PLAY_AREA_ROWS; r++) { for (var c = 0; c < PLAY_AREA_COLS; c++) { if (playArea[r][c] === self) { cardRow = r; cardCol = c; break; } } if (cardRow !== -1) { break; } } if (cardRow !== -1) { var totalBoostPercentage = 0; var neighbors = [[cardRow - 1, cardCol], [cardRow + 1, cardCol], [cardRow, cardCol - 1], [cardRow, cardCol + 1]]; for (var i = 0; i < neighbors.length; i++) { var nRow = neighbors[i][0]; var nCol = neighbors[i][1]; if (nRow >= 0 && nRow < PLAY_AREA_ROWS && nCol >= 0 && nCol < PLAY_AREA_COLS) { var neighborCard = playArea[nRow][nCol]; if (neighborCard && neighborCard.cardData.suit === 'diamonds' && ModSystem.getEquippedModAsset('diamonds', neighborCard.isPlayerCard) === 'boostDiamondsMod') { // Each level of the booster card adds 10% damage. totalBoostPercentage += neighborCard.level * 0.10; } } } if (totalBoostPercentage > 0) { damageToDeal = Math.floor(damageToDeal * (1 + totalBoostPercentage)); } } // Check if this is a clubs card with spreadshot mod equipped var isSpreadshot = self.cardData.suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', self.isPlayerCard) === 'spreadClubMod'; if (isSpreadshot) { // Spreadshot: reduced damage but hits multiple targets var spreadDamage = Math.floor(self.damage * 0.6); // 30% damage reduction var maxTargets = Math.min(1 + self.level, 5); // 1 + level targets, max 5 var allTargets; if (gameMode === 'coop') { allTargets = activePlayerChips.concat(activeAIChips); } else { allTargets = self.isPlayerCard ? activePlayerChips : activeAIChips; } var closestTargets = []; // This will store {chip, distanceSq} objects var turningPointY = SCREEN_HEIGHT / 2 - 200; // Find multiple targets using an efficient selection method instead of sorting all targets for (var i = 0; i < allTargets.length; i++) { var chip = allTargets[i]; if (chip.active && chip.health > 0 && chip.visible) { if (gameMode === 'coop') { var isPlayerChip = chip.isPlayerSide; var isPlayerCard = self.isPlayerCard; var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY; if (!onHorizontalPath) { if (isPlayerCard !== isPlayerChip) { continue; } } } var dx = chip.x - self.x; var dy = chip.y - self.y; var distanceSq = dx * dx + dy * dy; if (closestTargets.length < maxTargets) { closestTargets.push({ chip: chip, distanceSq: distanceSq }); } else { var furthestDistSq = -1; var furthestIdx = -1; for (var j = 0; j < closestTargets.length; j++) { if (closestTargets[j].distanceSq > furthestDistSq) { furthestDistSq = closestTargets[j].distanceSq; furthestIdx = j; } } if (distanceSq < furthestDistSq) { closestTargets[furthestIdx] = { chip: chip, distanceSq: distanceSq }; } } } } // Fire at the found targets for (var i = 0; i < closestTargets.length; i++) { var bullet = PoolManager.getBullet(); if (bullet) { bullet.activate(self.x, self.y, closestTargets[i].chip, spreadDamage, self.cardData.suit, self.isPlayerCard, self.level); gameLayer.addChild(bullet); activeBullets.push(bullet); } } } else { // Normal single-target firing var bullet = PoolManager.getBullet(); if (bullet) { bullet.activate(self.x, self.y, target, damageToDeal, self.cardData.suit, self.isPlayerCard, self.level); gameLayer.addChild(bullet); activeBullets.push(bullet); } } } // Play shoot sound for player cards if (self.isPlayerCard) { LK.getSound('shootSound').play(); } self.lastFired = LK.ticks; // Visual feedback for firing (enhanced for spreadshot) tween.stop(self, { scaleX: true, scaleY: true }); var isSpreadshot = self.cardData.suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', self.isPlayerCard) === 'spreadClubMod'; var scaleAmount = isSpreadshot ? 0.8 : 0.9; // More dramatic scale for spreadshot tween(self, { scaleX: scaleAmount, scaleY: scaleAmount }, { duration: 100, easing: tween.quadOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 150, easing: tween.elasticOut }); } }); }; self.update = function () { if (!self.isInPlay) { return; } // NEW: Handle Investment Hearts Mod if (self.cardData.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', self.isPlayerCard) === 'investmentHeartsMod') { // Generate income every 5 seconds (300 ticks) var incomeInterval = 300; if (LK.ticks - self.lastIncomeTick >= incomeInterval) { self.lastIncomeTick = LK.ticks; // timeOnBoard is a counter of income ticks, not actual ticks on board self.timeOnBoard++; // Income formula: 1 chip base, +1 for every 2 intervals (10 seconds). Max 5. var income = 1 + Math.floor(self.timeOnBoard / 2); income = Math.min(income, 5); // Cap income if (self.isPlayerCard) { gameState.playerChips += income; createFloatingText('+' + income, self.x, self.y - 70, 0xffd700, 30); updateUI(); } else { // AI gets income too gameState.aiChips += income; } } } if (LK.ticks - self.lastFired >= self.fireRate) { self.fire(); } }; // Initialize with level 1 self.setLevel(1); return self; }); var Mine = Container.expand(function () { var self = Container.call(this); self.active = false; self.damage = 50; self.isPlayerMine = true; self.explosionRadiusSq = 150 * 150; var mineGraphics = self.attachAsset('mine', { anchorX: 0.5, anchorY: 0.5 }); mineGraphics.scale.set(0.8); self.activate = function (x, y, damage, isPlayerMine) { self.active = true; self.visible = true; self.x = x; self.y = y; self.damage = damage; self.isPlayerMine = isPlayerMine; self.scale.set(0.1); tween(self, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.backOut }); }; self.explode = function () { if (!self.active) { return; } self.active = false; createExplosion(self.x, self.y, 0xffa500); var targets; if (gameMode === 'coop') { targets = activePlayerChips.concat(activeAIChips); } else { targets = self.isPlayerMine ? activePlayerChips : activeAIChips; } var turningPointY = SCREEN_HEIGHT / 2 - 200; for (var i = targets.length - 1; i >= 0; i--) { var chip = targets[i]; if (chip.active) { if (gameMode === 'coop') { var isPlayerChip = chip.isPlayerSide; var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY; if (!onHorizontalPath) { if (self.isPlayerMine !== isPlayerChip) { continue; } } } var dx = chip.x - self.x; var dy = chip.y - self.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < self.explosionRadiusSq) { chip.takeDamage(self.damage, self.isPlayerMine); } } } PoolManager.returnMine(self); }; self.update = function () { if (!self.active) { return; } var targets; if (gameMode === 'coop') { targets = activePlayerChips.concat(activeAIChips); } else { targets = self.isPlayerMine ? activePlayerChips : activeAIChips; } var collisionRadiusSq = 45 * 45; var turningPointY = SCREEN_HEIGHT / 2 - 200; for (var i = 0; i < targets.length; i++) { var chip = targets[i]; if (chip.active) { if (gameMode === 'coop') { var isPlayerChip = chip.isPlayerSide; var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY; if (!onHorizontalPath) { if (self.isPlayerMine !== isPlayerChip) { continue; } } } var dx = chip.x - self.x; var dy = chip.y - self.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < collisionRadiusSq) { self.explode(); break; } } } }; return self; }); /**** * Poker Chip Enemy Class ****/ var PokerChip = Container.expand(function () { var self = Container.call(this); self.active = false; self.health = 100; // Increased base health self.maxHealth = 100; self.value = 1; self.speed = 0.05; self.pathProgress = 0; self.isPlayerSide = true; self.damageFlashTimer = 0; self.burnDamage = 0; self.burnDuration = 0; self.burnTickTimer = 0; self.burnSourceIsPlayer = null; self.burnTickInterval = 30; // Burn ticks every 0.5 seconds (30 ticks at 60fps) self.slowDuration = 0; self.slowAmount = 0; self.freezeDuration = 0; self.originalSpeed = 0; self.iceCube = null; // Will hold the ice cube graphic self.freezeImmunityTimer = 0; self.slowEffectIcon = null; var chipGraphicsAssets = { 1: self.attachAsset('yellowChip', { anchorX: 0.5, anchorY: 0.5 }), 5: self.attachAsset('redChip', { anchorX: 0.5, anchorY: 0.5 }), 10: self.attachAsset('greenChip', { anchorX: 0.5, anchorY: 0.5 }), 25: self.attachAsset('blueChip', { anchorX: 0.5, anchorY: 0.5 }), 100: self.attachAsset('purpleChip', { anchorX: 0.5, anchorY: 0.5 }) }; var chipGraphics = null; // This will hold the current visible chip for (var val in chipGraphicsAssets) { chipGraphicsAssets[val].visible = false; } var healthText = new Text2('', { size: 80, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 8 }); healthText.anchor.set(0.5, 0.5); self.addChild(healthText); self.applyBurn = function (damage, duration, isPlayerSource) { // Stack burn damage but refresh duration self.burnDamage += damage; self.burnDuration = Math.max(self.burnDuration, duration); self.burnTickTimer = 0; if (isPlayerSource !== undefined) { self.burnSourceIsPlayer = isPlayerSource; } // REMOVED: Visual tint feedback - only use particles now // if (chipGraphics) { // chipGraphics.tint = 0xff4400; // Orange tint for burning // } // Create burn effect particles createBurnEffect(self.x, self.y); }; self.applyFreeze = function (duration) { // Return if chip is immune to freeze if (self.freezeImmunityTimer > 0) { return; } // Don't freeze if already frozen - with special logic for bosses if (self.freezeDuration > 0) { if (!self.isBoss) { // Non-bosses can refresh duration self.freezeDuration = Math.max(self.freezeDuration, duration); } // Bosses simply ignore the new freeze request if they are already frozen. return; } self.freezeDuration = duration; // Store original speed and stop movement if (self.slowDuration <= 0) { self.originalSpeed = self.speed; } self.speed = 0; // Create ice cube encasement if (!self.iceCube) { self.iceCube = LK.getAsset('iceCube', { anchorX: 0.5, anchorY: 0.5 }); self.iceCube.scale.set(1.2); // Slightly larger than chip self.iceCube.alpha = 0.8; self.addChild(self.iceCube); } self.iceCube.visible = true; // Ice formation animation self.iceCube.scale.set(0.1); self.iceCube.alpha = 0; tween(self.iceCube, { scaleX: 1.2, scaleY: 1.2, alpha: 0.8 }, { duration: 300, easing: tween.backOut }); }; self.processFreeze = function () { // Process immunity first, which ticks down every frame. if (self.freezeImmunityTimer > 0) { self.freezeImmunityTimer--; } if (self.freezeDuration <= 0) { return; } self.freezeDuration--; // Create occasional ice sparkle effects if (Math.random() < 0.1) { createIceSparkles(self.x, self.y); } // When freeze expires, break the ice if (self.freezeDuration <= 0) { if (self.slowDuration > 0) { self.speed = self.originalSpeed * (1 - self.slowAmount); } else { self.speed = self.originalSpeed; } self.freezeImmunityTimer = 120; // Apply 2-second immunity (120 ticks) if (self.iceCube) { // Ice breaking animation tween(self.iceCube, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 200, easing: tween.quadOut, onFinish: function onFinish() { self.iceCube.visible = false; } }); // Create ice shatter particles createIceShatterEffect(self.x, self.y); } } }; self.processBurnDamage = function () { if (self.burnDuration <= 0) { // Ensure burn is completely cleared self.burnDamage = 0; self.burnTickTimer = 0; return; } self.burnTickTimer++; if (self.burnTickTimer >= self.burnTickInterval) { self.burnTickTimer = 0; self.burnDuration--; // Apply burn damage (10% of original hit damage) var actualBurnDamage = Math.ceil(self.burnDamage); if (gameMode === 'coop' && self.burnSourceIsPlayer !== null) { var damageAmount = Math.min(actualBurnDamage, self.health); if (self.burnSourceIsPlayer) { playerTotalDamage += damageAmount; } else { aiTotalDamage += damageAmount; } } self.health -= actualBurnDamage; self.updateHealthText(); // Create floating burn damage text createFloatingText('-' + actualBurnDamage, self.x + (Math.random() - 0.5) * 40, self.y - 30, 0xff4400, 30); // Burn particles createBurnEffect(self.x, self.y); if (self.health <= 0) { self.active = false; self.die(); return; } // Reduce burn damage over time (burn weakens) self.burnDamage *= 0.9; // Clear burn when duration expires - NO TINT MANAGEMENT if (self.burnDuration <= 0) { self.burnDamage = 0; self.burnTickTimer = 0; // Also reset tick timer // REMOVED: No tint reset needed // if (chipGraphics && self.damageFlashTimer <= 0) { // chipGraphics.tint = 0xffffff; // } } } }; self.applySlow = function (amount, duration) { // A new slow can be applied if it's stronger, or to refresh duration. // We won't let slow override freeze. if (self.freezeDuration > 0) { return; } if (self.slowDuration <= 0) { // Not currently slowed, so store the original speed self.originalSpeed = self.speed; // Apply visual effects if (chipGraphics) { chipGraphics.tint = 0xaaaaaa; } if (!self.slowEffectIcon) { self.slowEffectIcon = LK.getAsset('slowClubsMod', { anchorX: 0.5, anchorY: 0.5 }); self.slowEffectIcon.scale.set(0.7); self.addChild(self.slowEffectIcon); } self.slowEffectIcon.visible = true; } self.slowDuration = duration; self.slowAmount = amount; // Apply slow relative to the original speed before any slows were applied. self.speed = self.originalSpeed * (1 - self.slowAmount); }; self.processSlow = function () { if (self.slowDuration > 0) { self.slowDuration--; if (self.slowDuration <= 0) { self.slowAmount = 0; // Slow has expired. If not frozen, restore speed. if (self.freezeDuration <= 0) { self.speed = self.originalSpeed; } // Remove visual effects if (chipGraphics && self.damageFlashTimer <= 0) { chipGraphics.tint = 0xffffff; } if (self.slowEffectIcon) { self.slowEffectIcon.visible = false; } } } }; self.activate = function (value, isPlayerSide, startPos) { self.active = true; self.visible = true; self.value = value; self.isPlayerSide = isPlayerSide; // Mark boss chips (they have much higher values than normal chips) self.isBoss = value >= 100; // Health scales with chip value - more valuable chips are tankier self.maxHealth = value * 50; // Health scaling per wave - NOW UNIFIED FOR BOTH MODES // Increase health by 50% after each boss/mini-boss wave (every 5 waves) var healthMultiplier = Math.pow(1.5, Math.floor((WaveSystem.waveNumber - 1) / 5)); self.maxHealth = self.maxHealth * healthMultiplier; // In both modes, bosses have 30% less health to keep them manageable if (self.isBoss) { self.maxHealth *= 0.7; // In PvP, bosses and mini-bosses get 20% more health if (typeof gameMode !== 'undefined' && gameMode === 'pvp') { self.maxHealth *= 1.2; } } self.health = self.maxHealth; self.pathProgress = 0; // NEW: Reset burn status to ensure clean state self.burnDamage = 0; self.burnDuration = 0; self.burnTickTimer = 0; self.burnSourceIsPlayer = null; // NEW: Reset freeze status to ensure clean state self.slowDuration = 0; self.slowAmount = 0; self.freezeDuration = 0; self.originalSpeed = 0; if (self.iceCube) { self.iceCube.visible = false; } if (self.slowEffectIcon) { self.slowEffectIcon.visible = false; } self.freezeImmunityTimer = 0; // Set all chips to yellow chip speed self.speed = 0.03; // Reset damage flash timer self.damageFlashTimer = 0; // Recreate healthText to ensure proper styling if (healthText && healthText.parent) { healthText.parent.removeChild(healthText); } healthText = new Text2('', { size: 80, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 8 }); healthText.anchor.set(0.5, 0.5); self.addChild(healthText); self.setChipAppearance(); self.x = startPos.x; self.y = startPos.y; }; self.updateHealthText = function () { // Remove the old text object if (healthText && healthText.parent) { healthText.parent.removeChild(healthText); } // Create a new text object with all styling properties healthText = new Text2(formatNumberWithSuffix(Math.max(0, self.health)), { size: 80, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 8 }); healthText.anchor.set(0.5, 0.5); self.addChild(healthText); }; self.setChipAppearance = function () { // Hide previous graphic and reset its scale to default if it exists if (chipGraphics) { chipGraphics.visible = false; chipGraphics.scale.set(1); } chipGraphics = chipGraphicsAssets[self.value] || chipGraphicsAssets[1]; if (chipGraphics) { chipGraphics.visible = true; // Apply boss scaling if (self.isBoss) { chipGraphics.scale.set(1.25); } } self.updateHealthText(); }; self.takeDamage = function (damage, isPlayerSource) { if (damage > 0) { createFloatingText('-' + Math.round(damage), self.x + (Math.random() - 0.5) * 40, self.y - 60, 0xff0000, 50); } if (gameMode === 'coop' && isPlayerSource !== undefined) { var damageAmount = Math.min(damage, self.health); if (isPlayerSource) { playerTotalDamage += damageAmount; } else { aiTotalDamage += damageAmount; } } self.health -= damage; self.updateHealthText(); self.damageFlashTimer = 10; // Flash for 10 ticks instead of setTimeout if (self.health <= 0) { // IMMEDIATELY mark as inactive to prevent further hits self.active = false; // Call kill counter handler before die() removes chip handleChipKill(self.isPlayerSide); self.die(); } }; self.die = function () { // Sound effect removed - no longer play pokerChipDie sound var chipsEarned = Math.ceil(self.value * 1.5); if (self.isPlayerSide) { var greedBonus = calculateGreedBonus(true, chipsEarned); var totalEarned = chipsEarned + greedBonus; gameState.playerChips += totalEarned; if (greedBonus > 0) { createFloatingText('+' + formatNumberWithSuffix(totalEarned) + ' (+' + formatNumberWithSuffix(greedBonus) + ' greed)', self.x, self.y - 30, 0xffd700, 35); } } else { var greedBonus = calculateGreedBonus(false, chipsEarned); gameState.aiChips += chipsEarned + greedBonus; } PoolManager.returnChip(self); }; self.update = function () { if (!self.active) { return; } // Handle damage flash (keep this for regular damage) if (self.damageFlashTimer > 0) { self.damageFlashTimer--; if (chipGraphics) { if (self.damageFlashTimer > 0) { chipGraphics.tint = 0xff0000; } else { // Flash is over, restore correct tint if (self.slowDuration > 0) { chipGraphics.tint = 0xaaaaaa; } else { chipGraphics.tint = 0xffffff; } } } } // NEW: Process burn damage self.processBurnDamage(); // NEW: Process freeze effect self.processFreeze(); // NEW: Process slow effect self.processSlow(); if (!self.active) { return; } self.pathProgress += self.speed; var pathPos = PathSystem.getPositionAlongPath(self.pathProgress, self.isPlayerSide); var isDefeated = pathPos.completed; if (!isDefeated && gameMode === 'coop') { // In coop mode, enemies are also defeated if they pass the right edge of the background. var rightEdge = coopBackground.x + coopBackground.width / 2; if (pathPos.x > rightEdge) { isDefeated = true; } } if (isDefeated) { // Boss chips remove 2 hearts instead of 1 var heartsToRemove = self.isBoss ? 2 : 1; if (gameMode === 'coop') { gameState.playerLives -= heartsToRemove; gameState.aiLives = gameState.playerLives; } else { if (self.isPlayerSide) { gameState.playerLives -= heartsToRemove; } else { gameState.aiLives -= heartsToRemove; } } LK.effects.flashScreen(0xff0000, 300); PoolManager.returnChip(self); return; } self.x = pathPos.x; self.y = pathPos.y; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0f3d0f }); /**** * Game Code ****/ /**** * Game Constants ****/ /**** * Poker Tower Defense - Complete Refactor ****/ // Helper function to calculate distance from point to line segment function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray2(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray2(r, a) { if (r) { if ("string" == typeof r) { return _arrayLikeToArray2(r, a); } var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray2(r, a) : void 0; } } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) { return Array.from(r); } } function _arrayWithoutHoles(r) { if (Array.isArray(r)) { return _arrayLikeToArray2(r); } } function _arrayLikeToArray2(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) { n[e] = r[e]; } return n; } function distancePointToLine(px, py, x1, y1, x2, y2) { var A = px - x1; var B = py - y1; var C = x2 - x1; var D = y2 - y1; var dot = A * C + B * D; var lenSq = C * C + D * D; if (lenSq === 0) { // Line segment is actually a point return Math.sqrt(A * A + B * B); } var param = dot / lenSq; var xx, yy; if (param < 0) { xx = x1; yy = y1; } else if (param > 1) { xx = x2; yy = y2; } else { xx = x1 + param * C; yy = y1 + param * D; } var dx = px - xx; var dy = py - yy; return Math.sqrt(dx * dx + dy * dy); } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) { throw o; } } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) { return _arrayLikeToArray(r, a); } var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) { n[e] = r[e]; } return n; } var SCREEN_WIDTH = 2048; var SCREEN_HEIGHT = 2732; var PLAY_AREA_COLS = 5; var PLAY_AREA_ROWS = 2; var SLOT_WIDTH = 300; var SLOT_HEIGHT = 420; var DEAL_SLOT_WIDTH = 240; var DEAL_SLOT_HEIGHT = 330; // AI area positioning (top) var AI_AREA_X = (SCREEN_WIDTH - PLAY_AREA_COLS * SLOT_WIDTH) / 2; var AI_AREA_Y = 150; // Player area positioning (middle, with plenty of room below) var PLAYER_AREA_X = (SCREEN_WIDTH - PLAY_AREA_COLS * SLOT_WIDTH) / 2; var PLAYER_AREA_Y = SCREEN_HEIGHT - 1300; // Much higher up // Player deal area (hand slots) - below play area var PLAYER_DEAL_AREA_Y = PLAYER_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT + 100; /**** * Card System ****/ var CardSystem = { suits: ['hearts', 'diamonds', 'clubs', 'spades'], values: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'], suitColors: { 'hearts': 0xff0000, 'diamonds': 0xff0000, 'clubs': 0x000000, 'spades': 0x000000 }, createDeck: function createDeck() { var deck = []; var _iterator = _createForOfIteratorHelper(this.suits), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var suit = _step.value; var _iterator2 = _createForOfIteratorHelper(this.values), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var value = _step2.value; deck.push({ suit: suit, value: value, id: suit + '_' + value }); } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } // Add jokers } catch (err) { _iterator.e(err); } finally { _iterator.f(); } deck.push({ suit: 'joker', value: 'red', id: 'joker_red' }); deck.push({ suit: 'joker', value: 'black', id: 'joker_black' }); return this.shuffleDeck(deck); }, shuffleDeck: function shuffleDeck(deck) { for (var i = deck.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = deck[i]; deck[i] = deck[j]; deck[j] = temp; } return deck; }, getCardValue: function getCardValue(card) { if (card.cardData.suit === 'joker') { return 14; } // Jokers are highest if (card.cardData.value === 'A') { return 14; } // Aces high if (card.cardData.value === 'K') { return 13; } if (card.cardData.value === 'Q') { return 12; } if (card.cardData.value === 'J') { return 11; } return parseInt(card.cardData.value); }, evaluatePokerHand: function evaluatePokerHand(cards) { var _this = this; if (!cards || cards.length === 0) { return { type: 'none', strength: 0, multiplier: 1, contributingCards: [] }; } // Sort cards by value for easier analysis var sortedCards = cards.slice().sort(function (a, b) { return _this.getCardValue(b) - _this.getCardValue(a); }); var values = sortedCards.map(function (card) { return _this.getCardValue(card); }); var suits = sortedCards.map(function (card) { return card.cardData.suit; }); // Count values and suits var valueCounts = {}; var suitCounts = {}; values.forEach(function (value) { return valueCounts[value] = (valueCounts[value] || 0) + 1; }); suits.forEach(function (suit) { return suitCounts[suit] = (suitCounts[suit] || 0) + 1; }); var counts = Object.values(valueCounts).sort(function (a, b) { return b - a; }); // Only check for 5-card hands if we have 5 cards var isFlush = false; var isStraight = false; if (cards.length === 5) { isFlush = Object.keys(suitCounts).length === 1; isStraight = this.checkStraight(values); } // Royal Flush if (isFlush && isStraight && values[0] === 14 && values[4] === 10) { return { type: 'royal_flush', strength: 10, multiplier: 25, contributingCards: sortedCards }; } // Straight Flush if (isFlush && isStraight) { return { type: 'straight_flush', strength: 9, multiplier: 12, contributingCards: sortedCards }; } // Four of a Kind if (counts[0] >= 4) { var quadValue; for (var v in valueCounts) { if (valueCounts[v] >= 4) { quadValue = parseInt(v); break; } } return { type: 'four_of_a_kind', strength: 8, multiplier: 8, contributingCards: sortedCards.filter(function (c) { return _this.getCardValue(c) === quadValue; }) }; } // Full House if (counts[0] === 3 && counts[1] === 2) { return { type: 'full_house', strength: 7, multiplier: 5, contributingCards: sortedCards }; } // Flush if (isFlush) { return { type: 'flush', strength: 6, multiplier: 3.5, contributingCards: sortedCards }; } // Straight if (isStraight) { return { type: 'straight', strength: 5, multiplier: 2.5, contributingCards: sortedCards }; } // Three of a Kind if (counts[0] === 3) { var tripValue; for (var v in valueCounts) { if (valueCounts[v] === 3) { tripValue = parseInt(v); break; } } return { type: 'three_of_a_kind', strength: 4, multiplier: 2, contributingCards: sortedCards.filter(function (c) { return _this.getCardValue(c) === tripValue; }) }; } // Two Pair if (counts[0] === 2 && counts[1] === 2) { var pairValues = []; for (var v in valueCounts) { if (valueCounts[v] === 2) { pairValues.push(parseInt(v)); } } return { type: 'two_pair', strength: 3, multiplier: 1.5, contributingCards: sortedCards.filter(function (c) { return pairValues.indexOf(_this.getCardValue(c)) !== -1; }) }; } // One Pair if (counts[0] === 2) { var pairValue; for (var v in valueCounts) { if (valueCounts[v] === 2) { pairValue = parseInt(v); break; } } return { type: 'one_pair', strength: 2, multiplier: 1.2, contributingCards: sortedCards.filter(function (c) { return _this.getCardValue(c) === pairValue; }) }; } // High Card return { type: 'high_card', strength: 1, multiplier: 1, contributingCards: [sortedCards[0]] }; }, checkStraight: function checkStraight(values) { if (values.length !== 5) { return false; } // Check for ace-low straight (A, 2, 3, 4, 5) if (values[0] === 14 && values[1] === 5 && values[2] === 4 && values[3] === 3 && values[4] === 2) { return true; } // Check normal straight for (var i = 0; i < 4; i++) { if (values[i] - values[i + 1] !== 1) { return false; } } return true; } }; /**** * Object Pool Manager ****/ var PoolManager = { chipPool: [], bulletPool: [], cardPool: [], playerMinePool: [], aiMinePool: [], CHIP_POOL_SIZE: 50, BULLET_POOL_SIZE: 100, CARD_POOL_SIZE: 60, MINE_POOL_SIZE: 30, // 30 per side init: function init() { // Initialize pools for (var i = 0; i < this.CHIP_POOL_SIZE; i++) { var chip = new PokerChip(); chip.active = false; chip.visible = false; this.chipPool.push(chip); } for (var i = 0; i < this.BULLET_POOL_SIZE; i++) { var bullet = new Bullet(); bullet.active = false; bullet.visible = false; this.bulletPool.push(bullet); } for (var i = 0; i < this.MINE_POOL_SIZE; i++) { var playerMine = new Mine(); playerMine.active = false; playerMine.visible = false; this.playerMinePool.push(playerMine); var aiMine = new Mine(); aiMine.active = false; aiMine.visible = false; this.aiMinePool.push(aiMine); } }, getChip: function getChip() { for (var i = 0; i < this.chipPool.length; i++) { if (!this.chipPool[i].active) { return this.chipPool[i]; } } return null; }, getBullet: function getBullet() { for (var i = 0; i < this.bulletPool.length; i++) { if (!this.bulletPool[i].active) { return this.bulletPool[i]; } } return null; }, getMine: function getMine(isPlayerMine) { var pool = isPlayerMine ? this.playerMinePool : this.aiMinePool; for (var i = 0; i < pool.length; i++) { if (!pool[i].active) { return pool[i]; } } return null; }, returnChip: function returnChip(chip) { // Force-set inactive state chip.active = false; chip.visible = false; chip.health = 0; // Ensure health is 0 // Reset tint on all visual components of the chip. // This is to ensure that chip graphics don't retain the red damage flash tint // when being reused from the pool. if (chip.children) { for (var i = 0; i < chip.children.length; i++) { var child = chip.children[i]; if (child.tint !== undefined) { child.tint = 0xffffff; } } } // NEW: Clear burn status completely chip.burnDamage = 0; chip.burnDuration = 0; chip.burnTickTimer = 0; // Clear ownership flag var wasPlayerSide = chip.isPlayerSide; chip.isPlayerSide = null; // Remove from active arrays (check both arrays to be safe) var playerIndex = activePlayerChips.indexOf(chip); if (playerIndex !== -1) { activePlayerChips.splice(playerIndex, 1); } var aiIndex = activeAIChips.indexOf(chip); if (aiIndex !== -1) { activeAIChips.splice(aiIndex, 1); } // Remove from containers if (chip.parent) { chip.parent.removeChild(chip); } // Double-check removal from both containers if (activePlayerChipsContainer.children.indexOf(chip) !== -1) { activePlayerChipsContainer.removeChild(chip); } if (activeAIChipsContainer.children.indexOf(chip) !== -1) { activeAIChipsContainer.removeChild(chip); } }, returnBullet: function returnBullet(bullet) { var explosionColor = 0x333333; // Dark Grey for clubs/spades if (bullet.suit === 'hearts' || bullet.suit === 'diamonds') { explosionColor = 0xff0000; // Red for hearts/diamonds } createExplosion(bullet.x, bullet.y, explosionColor); bullet.active = false; bullet.visible = false; bullet.target = null; // Add this line to clear the target reference var index = activeBullets.indexOf(bullet); if (index !== -1) { activeBullets.splice(index, 1); } gameLayer.removeChild(bullet); }, returnMine: function returnMine(mine) { mine.active = false; mine.visible = false; var index = activeMines.indexOf(mine); if (index !== -1) { activeMines.splice(index, 1); } if (mine.parent) { mine.parent.removeChild(mine); } } }; /**** * Path System ****/ var PathSystem = { playerPath: [], aiPath: [], init: function init() { // Create player path - rectangular loop around the play area var padding = 130; // Distance from play area - increased by 50 pixels var verticalPadding = padding - 25 - 30; // Reduced by 25 + 30 to decrease side length by 110px total var leftX = PLAYER_AREA_X - padding + 10; var rightX = PLAYER_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH + padding - 20 - 10; var topY = PLAYER_AREA_Y - verticalPadding; var bottomY = PLAYER_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT + verticalPadding; this.playerPath = [ // Start at bottom left { x: leftX, y: bottomY }, // Go up the left side { x: leftX, y: topY }, // Go across the top { x: rightX, y: topY }, // Go down the right side { x: rightX, y: bottomY }]; // Create AI path - UPSIDE DOWN mirror of player path var aiLeftX = AI_AREA_X - padding; var aiRightX = AI_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH + padding; var aiVerticalPadding = verticalPadding - 40 + 15; // Additional 40px reduction on each side for AI (80px total), extended by 15px var aiTopY = AI_AREA_Y - aiVerticalPadding; var aiBottomY = AI_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT + aiVerticalPadding; this.aiPath = [ // Start at TOP left (opposite of player) { x: aiLeftX, y: aiTopY }, // Go DOWN the left side (opposite of player) { x: aiLeftX, y: aiBottomY }, // Go across the BOTTOM (opposite of player) { x: aiRightX, y: aiBottomY }, // Go UP the right side (opposite of player) { x: aiRightX, y: aiTopY }]; if (gameMode === 'coop') { var centerX = SCREEN_WIDTH / 2; var centerY = SCREEN_HEIGHT / 2; var turningPointY = centerY - 200; // 200 pixels up from center var goalX = SCREEN_WIDTH + 200; this.playerPath = [{ x: leftX, y: bottomY }, { x: leftX, y: turningPointY }, { x: goalX, y: turningPointY }]; this.aiPath = [{ x: leftX, y: aiTopY }, { x: leftX, y: turningPointY }, { x: goalX, y: turningPointY }]; } }, // Alternative offset method that spreads chips along the path direction: getPathStart: function getPathStart(isPlayerSide) { var baseStart = isPlayerSide ? this.playerPath[0] : this.aiPath[0]; var pathDirection = isPlayerSide ? this.playerPath[1] : this.aiPath[1]; // Calculate direction vector from start to next point var dx = pathDirection.x - baseStart.x; var dy = pathDirection.y - baseStart.y; var length = Math.sqrt(dx * dx + dy * dy); // Normalize direction vector var normalizedX = dx / length; var normalizedY = dy / length; // Random offset along the path (backward from start point) var pathOffset = Math.random() * 80; // 0-80 pixels back along path var sideOffset = (Math.random() - 0.5) * 40; // -20 to +20 pixels to the side return { x: baseStart.x - normalizedX * pathOffset + normalizedY * sideOffset, y: baseStart.y - normalizedY * pathOffset - normalizedX * sideOffset }; }, getPositionAlongPath: function getPositionAlongPath(progress, isPlayerSide) { var path = isPlayerSide ? this.playerPath : this.aiPath; var pathLength = this.calculatePathLength(path); var targetDistance = progress / 100 * pathLength; if (targetDistance >= pathLength) { return { x: path[path.length - 1].x, y: path[path.length - 1].y, completed: true }; } var currentDistance = 0; for (var i = 0; i < path.length - 1; i++) { var segmentLength = this.getDistance(path[i], path[i + 1]); if (currentDistance + segmentLength >= targetDistance) { var segmentProgress = (targetDistance - currentDistance) / segmentLength; return { x: path[i].x + (path[i + 1].x - path[i].x) * segmentProgress, y: path[i].y + (path[i + 1].y - path[i].y) * segmentProgress, completed: false }; } currentDistance += segmentLength; } return { x: path[path.length - 1].x, y: path[path.length - 1].y, completed: true }; }, calculatePathLength: function calculatePathLength(path) { var total = 0; for (var i = 0; i < path.length - 1; i++) { total += this.getDistance(path[i], path[i + 1]); } return total; }, getDistance: function getDistance(p1, p2) { var dx = p2.x - p1.x; var dy = p2.y - p1.y; return Math.sqrt(dx * dx + dy * dy); } }; /**** * Chip Spawner ****/ var ChipSpawner = { spawnChip: function spawnChip(value, isPlayerSide) { var chip = PoolManager.getChip(); if (!chip) { return; } var activeChips = isPlayerSide ? activePlayerChips : activeAIChips; var minDistance = 85; // Half the chip diameter (170/2) to allow controlled overlap var minDistanceSq = minDistance * minDistance; var newPos; // If no other chips exist, we can place it anywhere. if (activeChips.length === 0) { newPos = PathSystem.getPathStart(isPlayerSide); } else { var maxAttempts = 25; // Try to find a free spot with random placement first. It's fast. var foundPosition = false; for (var attempt = 0; attempt < maxAttempts; attempt++) { var candidatePos = PathSystem.getPathStart(isPlayerSide); var isValid = true; for (var i = 0; i < activeChips.length; i++) { var otherChip = activeChips[i]; var dx = candidatePos.x - otherChip.x; var dy = candidatePos.y - otherChip.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistanceSq) { isValid = false; break; } } if (isValid) { newPos = candidatePos; foundPosition = true; break; } } // If random placements failed, use a more deterministic spiral search. if (!foundPosition) { var baseStart = PathSystem.getPathStart(isPlayerSide); var searchRadius = minDistance; // Start searching just outside the minimum distance. var angleStep = Math.PI / 6; // Check 12 directions. var spiralAttempts = 30; for (var i = 0; i < spiralAttempts; i++) { for (var angle = 0; angle < Math.PI * 2; angle += angleStep) { var testX = baseStart.x + Math.cos(angle) * searchRadius; var testY = baseStart.y + Math.sin(angle) * searchRadius; var isCandidateValid = true; for (var j = 0; j < activeChips.length; j++) { var otherChip = activeChips[j]; var dx = testX - otherChip.x; var dy = testY - otherChip.y; var distanceSq = dx * dx + dy * dy; if (distanceSq < minDistanceSq) { isCandidateValid = false; break; } } if (isCandidateValid) { newPos = { x: testX, y: testY }; foundPosition = true; break; } } if (foundPosition) { break; } searchRadius += 20; // Spiral outwards. } } // Ultimate fallback if no position is found (should be very rare). if (!foundPosition) { newPos = PathSystem.getPathStart(isPlayerSide); } } chip.activate(value, isPlayerSide, newPos); // IMPORTANT: Add chip to active array BEFORE the container // This ensures that subsequent chips spawned in the same batch // will detect this chip when checking for overlaps if (isPlayerSide) { activePlayerChips.push(chip); activePlayerChipsContainer.addChildAt(chip, 0); } else { activeAIChips.push(chip); activeAIChipsContainer.addChildAt(chip, 0); } }, spawnChipAtPosition: function spawnChipAtPosition(value, isPlayerSide, position) { var chip = PoolManager.getChip(); if (!chip) { return; } chip.activate(value, isPlayerSide, position); // Add chip to active array and container if (isPlayerSide) { activePlayerChips.push(chip); activePlayerChipsContainer.addChildAt(chip, 0); } else { activeAIChips.push(chip); activeAIChipsContainer.addChildAt(chip, 0); } } }; /**** * Mod System ****/ var ModSystem = { equippedMods: { hearts: null, diamonds: null, clubs: null, spades: null }, aiEquippedMods: { hearts: null, diamonds: null, clubs: null, spades: null }, modData: { burnHeartMod: { suit: 'hearts', name: 'Flame', description: 'Hearts bullets apply burning damage over time. Burn damage is 10% of hit damage per tick for 4 seconds. Burn effects stack.' }, unityHeartsMod: { suit: 'hearts', name: 'Unity', description: 'Increases bullet damage by 25% for each adjacent Hearts card.' }, chipsDiamondMod: { suit: 'diamonds', name: 'Greed', description: 'Earn bonus chips when enemies are defeated, based on diamonds on board. Bonus scales with card level.' }, gamblerDiamondsMod: { suit: 'diamonds', name: 'Gambler', description: 'Diamonds have a chance to deal double damage or none at all. The chance for double damage increases with card level.' }, boostDiamondsMod: { suit: 'diamonds', name: 'Boost', description: 'Diamonds boost the attack power of adjacent cards. The boost increases with the card\'s level.' }, freezeSpadeMod: { suit: 'spades', name: 'Freeze', description: 'Spades have a chance to freeze enemies in place when dealing damage. Duration scales with card level.' }, spreadClubMod: { suit: 'clubs', name: 'Spreadshot', description: 'Clubs deal reduced damage but hit multiple enemies. Extra targets scale with card level.' }, slowClubsMod: { suit: 'clubs', name: 'Slow', description: 'Clubs bullets slow enemies on hit. Slow amount and duration scale with card level.' }, ricochetClubsMod: { suit: 'clubs', name: 'Ricochet', description: 'Clubs bullets ricochet amongst enemies, with limited bounces, dealing less damage with each one.' }, mineSpadesMod: { suit: 'spades', name: 'Mine', description: 'Spades cards place mines on the enemy path that explode on contact, dealing area damage.' }, deathSpadesMod: { suit: 'spades', name: 'Execute', description: 'When equipped spades bullets have a chance to execute low health targets immediately.' }, investmentHeartsMod: { suit: 'hearts', name: 'Investment', description: 'Hearts cards generate income while on board. Income increases the longer the card is on board and resets on level up.' } }, currentlyDisplayedMod: null, modDisplayContainer: null, topSuitGraphics: [], ownedMods: {}, // Store references to top suit graphics init: function init() { if (storage.equippedMods) { this.equippedMods = storage.equippedMods; } if (storage.ownedMods) { this.ownedMods = storage.ownedMods; } }, saveToStorage: function saveToStorage() { storage.equippedMods = this.equippedMods; storage.ownedMods = this.ownedMods; }, isModOwned: function isModOwned(modAssetId) { return !!this.ownedMods[modAssetId]; }, unlockMod: function unlockMod(modAssetId) { this.ownedMods[modAssetId] = true; this.saveToStorage(); }, getUnownedMods: function getUnownedMods() { var unowned = []; for (var modId in this.modData) { if (!this.isModOwned(modId)) { unowned.push(modId); } } return unowned; }, equipMod: function equipMod(modAssetId) { var modData = this.modData[modAssetId]; if (!modData) { return; } this.equippedMods[modData.suit] = modAssetId; this.saveToStorage(); this.updateTopSuitDisplay(); this.hideModDisplay(); }, updateTopSuitDisplay: function updateTopSuitDisplay() { var suitAssets = ['heartSuit', 'diamondSuit', 'clubSuit', 'spadeSuit']; var suits = ['hearts', 'diamonds', 'clubs', 'spades']; for (var i = 0; i < this.topSuitGraphics.length; i++) { var container = this.topSuitGraphics[i]; if (container) { // Make it an Ace card if it's not already var hasAceText = false; for (var k = 0; k < container.children.length; k++) { if (container.children[k].isAceText) { hasAceText = true; break; } } if (!hasAceText) { var suitForAce = suits[i]; var suitColor = CardSystem.suitColors[suitForAce] || 0x000000; var valueText = new Text2('A', { size: 56 * 1.5, fill: suitColor, weight: 800, stroke: 0x000000, strokeThickness: 0 }); valueText.isAceText = true; valueText.anchor.set(0, 0); // The card background is scaled 1.5x, these coords are scaled to match. valueText.x = -95 * 1.5; valueText.y = -135 * 1.5; container.addChild(valueText); } // Remove the previous mod/suit icon if it exists for (var j = container.children.length - 1; j >= 0; j--) { if (container.children[j].isSuitIcon) { container.removeChildAt(j); break; } } // Add the appropriate icon (either mod or default suit) var suit = suits[i]; var equippedMod = this.equippedMods[suit]; var assetId = equippedMod || suitAssets[i]; var suitIcon = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); suitIcon.isSuitIcon = true; suitIcon.scale.set(1.2); // Center it in the background card suitIcon.x = 0; suitIcon.y = 0; container.addChild(suitIcon); } } }, showModDisplay: function showModDisplay(modAssetId, sourceX, sourceY) { if (this.currentlyDisplayedMod) { return; } var modData = this.modData[modAssetId]; if (!modData) { return; } this.currentlyDisplayedMod = modAssetId; this.modDisplayContainer = new Container(); this.modDisplayContainer.interactive = true; uiLayer.addChild(this.modDisplayContainer); // Add a semi-transparent background to catch clicks outside the popup var bgBlocker = new Container(); bgBlocker.interactive = true; bgBlocker.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); this.modDisplayContainer.addChild(bgBlocker); var enlargedMod = LK.getAsset(modAssetId, { anchorX: 0.5, anchorY: 0.5 }); enlargedMod.x = SCREEN_WIDTH / 2; enlargedMod.y = SCREEN_HEIGHT / 2 - 450; enlargedMod.scale.set(1); this.modDisplayContainer.addChild(enlargedMod); var overlayWidth = 600; var overlayHeight = 400; var descriptionOverlay = new Container(); var overlayBg = LK.getAsset('card', { anchorX: 0.5, anchorY: 0.5 }); overlayBg.scale.set(overlayWidth / 150, overlayHeight / 240); overlayBg.tint = 0x000000; overlayBg.alpha = 0.8; descriptionOverlay.addChild(overlayBg); var descText = new Text2(modData.description, { size: 50, fill: 0xffffff, weight: 600, stroke: 0x000000, strokeThickness: 2, align: 'center', wordWrap: true, wordWrapWidth: overlayWidth - 40 }); descText.anchor.set(0.5, 0); descText.y = -overlayHeight / 2 + 40; descriptionOverlay.addChild(descText); var equipButton = LK.getAsset('card', { anchorX: 0.5, anchorY: 0.5, interactive: true }); equipButton.scale.set(1.5, 0.5); equipButton.tint = 0x000000; equipButton.alpha = 0.8; equipButton.y = overlayHeight / 2 + 150; descriptionOverlay.addChild(equipButton); var equipText = new Text2('Equip', { size: 50, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 3 }); equipText.anchor.set(0.5, 0.5); equipText.y = equipButton.y; descriptionOverlay.addChild(equipText); descriptionOverlay.x = SCREEN_WIDTH / 2; descriptionOverlay.y = SCREEN_HEIGHT / 2 + 250; descriptionOverlay.alpha = 0; this.modDisplayContainer.addChild(descriptionOverlay); var self = this; var isOwned = this.isModOwned(modAssetId); if (isOwned) { equipText.setText('Equip'); equipButton.down = function () { self.equipMod(modAssetId); }; } else { equipText.setText('Not Owned'); equipButton.interactive = false; equipButton.tint = 0x333333; enlargedMod.tint = 0x808080; } tween(enlargedMod, { scaleX: 4, scaleY: 4 }, { duration: 300, easing: tween.backOut }); tween(descriptionOverlay, { alpha: 1 }, { duration: 200, delay: 150 }); bgBlocker.down = function () { self.hideModDisplay(); }; }, hideModDisplay: function hideModDisplay() { if (!this.modDisplayContainer) { return; } var self = this; tween(this.modDisplayContainer, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { if (self.modDisplayContainer && self.modDisplayContainer.parent) { self.modDisplayContainer.parent.removeChild(self.modDisplayContainer); } self.modDisplayContainer = null; self.currentlyDisplayedMod = null; } }); }, getEquippedModAsset: function getEquippedModAsset(suit, isPlayerCard) { if (isPlayerCard === undefined) { isPlayerCard = true; } var mods = isPlayerCard ? this.equippedMods : this.aiEquippedMods; return mods[suit] || null; } }; /**** * Wave Spawning System ****/ var WaveSystem = { playerSpawnTimer: 0, aiSpawnTimer: 0, waveNumber: 1, waveTimer: 0, waveDuration: 1800, // 30 seconds per wave (30 * 60 ticks) spawnInterval: 45, // Much faster: spawn every 0.75 seconds instead of 2 seconds bossSpawned: false, // Track if boss has been spawned this wave getChipValue: function getChipValue(waveNumber) { // Wave 1: Only 1-chips (easier) if (waveNumber === 1) { return 1; } // Wave 2-3: More 5s introduced earlier for increased difficulty else if (waveNumber <= 3) { return Math.random() < 0.2 ? 5 : 1; // 20% chance of 5-chip (increased from 10%) } // Wave 4-6: Even more 5s mixed in else if (waveNumber <= 6) { return Math.random() < 0.35 ? 5 : 1; // 35% chance of 5-chip (increased from 25%) } // Wave 7-9: More 5s than 1s for increased difficulty else if (waveNumber <= 9) { return Math.random() < 0.65 ? 5 : 1; // 65% chance of 5-chip (increased from 50%) } // Wave 11-15: Introduce 10-chips earlier and more frequently else if (waveNumber <= 15) { var rand = Math.random(); if (rand < 0.15) { return 10; } else if (rand < 0.7) { return 5; } else { return 1; } } // Wave 16-19: More variety with higher values else if (waveNumber <= 19) { var rand = Math.random(); if (rand < 0.3) { return 10; } else if (rand < 0.8) { return 5; } else { return 1; } } // Wave 21+: Keep scaling gradually with higher difficulty else if (!this.isBossWave(waveNumber)) { var rand = Math.random(); if (rand < 0.08) { return 25; } else if (rand < 0.4) { return 10; } else if (rand < 0.85) { return 5; } else { return 1; } } }, getBossChipValue: function getBossChipValue(waveNumber) { // Mini-boss waves (waves 5, 15, 25, etc.) if (waveNumber % 10 === 5) { var miniBossLevel = Math.floor(waveNumber / 10); var baseMiniBossValue = 100; // Purple chip value return baseMiniBossValue * Math.pow(2, miniBossLevel); } // Main boss waves (waves 10, 20, 30, etc.) var bossLevel = Math.floor(waveNumber / 10); // Base value derived from total health of wave 9 enemies var baseBossValue = 210; return baseBossValue * Math.pow(2, bossLevel - 1); }, isBossWave: function isBossWave(waveNumber) { // Both PvP and Coop now have mini-bosses every 5 waves and main bosses every 10 waves return waveNumber > 0 && (waveNumber % 10 === 0 || waveNumber % 10 === 5); }, spawnChip: function spawnChip(isPlayerSide) { if (this.isBossWave(this.waveNumber)) { // Boss wave: spawn ONE big enemy at the start, then nothing if (!this.bossSpawned) { var bossValue = this.getBossChipValue(this.waveNumber); ChipSpawner.spawnChip(bossValue, isPlayerSide); this.bossSpawned = true; console.log("BOSS spawned with value:", bossValue); } // Don't spawn anything else during boss wave return; } // Normal wave spawning - always single enemy var chipValue = this.getChipValue(this.waveNumber); ChipSpawner.spawnChip(chipValue, isPlayerSide); }, playerBossDefeated: false, aiBossDefeated: false, update: function update() { this.waveTimer++; // Check if wave is complete if (this.waveTimer >= this.waveDuration) { // If it's a boss wave, only advance if both bosses are defeated. if (this.isBossWave(this.waveNumber) && (!this.playerBossDefeated || !this.aiBossDefeated)) { // Don't end the wave, just let it continue until bosses are defeated. } else { // For normal waves, or for boss waves where both bosses are defeated. // Check if both bosses were defeated in a boss wave if (this.isBossWave(this.waveNumber) && this.playerBossDefeated && this.aiBossDefeated) { var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS"; console.log("Both " + bossType + "es defeated! Moving to next round!"); createFloatingText('ROUND COMPLETE!', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0x00ff00); // Reset boss defeat flags this.playerBossDefeated = false; this.aiBossDefeated = false; } this.waveTimer = 0; this.waveNumber++; this.playerSpawnTimer = 0; this.aiSpawnTimer = 0; this.bossSpawned = false; // Reset boss spawn flag var waveType; if (this.isBossWave(this.waveNumber)) { waveType = this.waveNumber % 10 === 5 ? "MINI-BOSS WAVE" : "BOSS WAVE"; } else { waveType = "Wave"; } console.log(waveType + " " + this.waveNumber + " starting!"); return; } } // BOSS WAVE LOGIC - Spawn bosses immediately at start of wave if (this.isBossWave(this.waveNumber)) { if (!this.bossSpawned && this.waveTimer === 1) { // Only on first tick of boss wave var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS"; console.log("=== " + bossType + " WAVE " + this.waveNumber + " STARTING ==="); // Spawn player boss var playerBossValue = this.getBossChipValue(this.waveNumber); ChipSpawner.spawnChip(playerBossValue, true); console.log("PLAYER " + bossType + " spawned with value:", playerBossValue, "Active player chips:", activePlayerChips.length); // Spawn AI boss (only in PvP mode - coop doesn't have AI bosses) if (gameMode === 'pvp') { var aiBossValue = this.getBossChipValue(this.waveNumber); ChipSpawner.spawnChip(aiBossValue, false); console.log("AI " + bossType + " spawned with value:", aiBossValue, "Active AI chips:", activeAIChips.length); } else { // In coop, AI side is automatically "defeated" since there are no AI enemies this.aiBossDefeated = true; } this.bossSpawned = true; } // Boss defeat detection - only after bosses have had time to spawn if (this.bossSpawned && this.waveTimer > 120) { // 2 second delay var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS"; console.log("Checking " + bossType.toLowerCase() + " status - Player chips:", activePlayerChips.length, "AI chips:", activeAIChips.length); // Check if player side boss is defeated if (!this.playerBossDefeated && activePlayerChips.length === 0) { this.playerBossDefeated = true; console.log("Player defeated the " + bossType.toLowerCase() + "!"); createFloatingText(bossType + ' DEFEATED!', SCREEN_WIDTH / 2, PLAYER_AREA_Y + SLOT_HEIGHT, 0xffd700); } // Check if AI side boss is defeated (only in PvP) if (gameMode === 'pvp' && !this.aiBossDefeated && activeAIChips.length === 0) { this.aiBossDefeated = true; console.log("AI defeated the " + bossType.toLowerCase() + "!"); createFloatingText('AI ' + bossType + ' DEFEATED!', SCREEN_WIDTH / 2, AI_AREA_Y + SLOT_HEIGHT, 0xffd700); } // If both bosses defeated, immediately end the wave if (this.playerBossDefeated && this.aiBossDefeated) { this.waveTimer = this.waveDuration - 1; // Set to end on next tick } } return; // Don't do normal spawning during boss waves } // NORMAL WAVE LOGIC var currentSpawnInterval = this.spawnInterval; if (this.waveNumber === 1) { currentSpawnInterval = 75; // Slightly faster spawning for wave 1 (1.25 seconds) } else if (this.waveNumber >= 2) { currentSpawnInterval = Math.max(45, 60 - Math.floor((this.waveNumber - 2) * 15)); // Wave 2 is slower (60), then speeds up to normal (45) for wave 3+ } // Spawn on player side this.playerSpawnTimer++; if (this.playerSpawnTimer >= currentSpawnInterval) { this.playerSpawnTimer = 0; this.spawnChip(true); } // Spawn on AI side this.aiSpawnTimer++; if (this.aiSpawnTimer >= currentSpawnInterval) { this.aiSpawnTimer = 0; this.spawnChip(false); } } }; /**** * Game State ****/ var gameState = { playerChips: 200, aiChips: 200, playerLives: 3, aiLives: 3, isPlayerTurn: true, dealCost: 25, // Lowered from 38 to 25 // Starting cost dealCount: 0, // ... rest remains the same playerDeck: [], playerHand: [], playerPlayArea: [], aiDeck: [], aiPlayArea: [] }; // Game state management var currentGameState = 'start'; // 'start' or 'playing' var startScreenElements = []; var gameElements = []; /**** * Game Variables ****/ var activePlayerChips = []; var activeAIChips = []; var activeBullets = []; var activeMines = []; var playerTotalDamage = 0; var aiTotalDamage = 0; var playerHandNameTexts = []; var backgroundSuits = []; var slotIndicators = []; var selectedCard = null; var isDragging = false; var originalCardPosition = null; var gameReady = false; var activePlayerChipsContainer = new Container(); // Container for player chips var activeAIChipsContainer = new Container(); // Container for AI chips var playerLifeHearts = []; var aiLifeHearts = []; var opponentNameText = null; var playerNameText = null; var lastPlayerLives = 0; var lastAiLives = 0; var gameMode = 'pvp'; var gameLayer = new Container(); var floorBackground = LK.getAsset('floorbackround', { anchorX: 0.5, anchorY: 0.5 }); floorBackground.x = SCREEN_WIDTH / 2; floorBackground.y = SCREEN_HEIGHT / 2; gameLayer.addChild(floorBackground); var uiLayer = new Container(); game.addChild(gameLayer); game.addChild(uiLayer); /**** * UI Elements ****/ var playerChipsText = new Text2('Chips: 200', { size: 50, fill: 0xffd700, weight: 800, stroke: 0x000000, strokeThickness: 0 }); playerChipsText.x = 50; playerChipsText.y = SCREEN_HEIGHT - 120; playerChipsText.visible = false; uiLayer.addChild(playerChipsText); gameElements.push(playerChipsText); // Add AI stats too for clarity var waveText = new Text2('Wave: 1', { size: 40, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 0 }); waveText.x = SCREEN_WIDTH - 200; waveText.y = 50; waveText.visible = false; uiLayer.addChild(waveText); gameElements.push(waveText); var discardAreaContainer = new Container(); var dealButtonGraphic = discardAreaContainer.attachAsset('dealButton', { anchorX: 0.5, anchorY: 0.5 }); var refundButtonGraphic = discardAreaContainer.attachAsset('refundButton', { anchorX: 0.5, anchorY: 0.5 }); refundButtonGraphic.visible = false; var discardAreaGraphic = dealButtonGraphic; var discardText = new Text2('-25', { size: 50, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 0 }); discardText.anchor.set(0.5, 0.5); discardText.y = 110; discardAreaContainer.addChild(discardText); // Position it right of the hand var handWidthForDiscard = 5 * DEAL_SLOT_WIDTH + 4 * 30; var handStartXForDiscard = (SCREEN_WIDTH - handWidthForDiscard) / 2; var discardX = handStartXForDiscard + handWidthForDiscard + 30 + DEAL_SLOT_WIDTH / 2; // Make sure it doesn't go off screen if (discardX + DEAL_SLOT_WIDTH / 2 > SCREEN_WIDTH) { discardX = SCREEN_WIDTH - DEAL_SLOT_WIDTH / 2 - 20; } discardAreaContainer.x = discardX + 10 + 10; discardAreaContainer.y = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2 + 10 - 10; discardAreaContainer.visible = false; uiLayer.addChild(discardAreaContainer); gameElements.push(discardAreaContainer); discardAreaContainer.down = function () { if (!isDragging && gameState.playerChips >= gameState.dealCost) { dealNewHand(); } }; /**** * Game Functions ****/ // --- Kill counter for extra enemy spawn (player kills only) --- var playerKillCounter = 0; function handleChipKill(isPlayerSide) { // Only count chip kills for extra enemy spawn in PvP mode if (gameMode !== 'pvp') { return; } // Only track player kills and spawn extra chips for the AI if (isPlayerSide) { playerKillCounter++; if (playerKillCounter >= 5) { playerKillCounter = 0; // Spawn extra enemy on AI side var chipValue = WaveSystem.getChipValue(WaveSystem.waveNumber); ChipSpawner.spawnChip(chipValue, false); } } } function dealInitialHand() { var cardsToDeal = 5; var dealDelay = 150; // ms var dealCard = function dealCard(i) { if (gameState.playerDeck.length === 0) { gameState.playerDeck = CardSystem.createDeck(); } var cardData = gameState.playerDeck.pop(); var startLevel = Math.floor((WaveSystem.waveNumber - 1) / 10) + 1; var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; var handStartX = (SCREEN_WIDTH - handWidth) / 2; var slotX = handStartX + i * DEAL_SLOT_WIDTH + i * 30 + DEAL_SLOT_WIDTH / 2; var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2; var startX = slotX; var startY = SCREEN_HEIGHT + DEAL_SLOT_HEIGHT; // Mark slot as occupied immediately to prevent overwriting if deal is pressed too quickly gameState.playerHand[i] = true; var cardBack = LK.getAsset('cardBack', { anchorX: 0.5, anchorY: 0.5 }); cardBack.x = startX; cardBack.y = startY; uiLayer.addChild(cardBack); // Animate card flying into place and flipping tween(cardBack, { x: slotX, y: slotY }, { duration: 250, easing: tween.cubicOut, onFinish: function onFinish() { // Flip animation: shrink back tween(cardBack, { scaleX: 0.01 }, { duration: 150, easing: tween.quadIn, onFinish: function onFinish() { if (cardBack.parent) { cardBack.parent.removeChild(cardBack); } // Create and show the real card var card = new Card(cardData); card.setLevel(startLevel); card.activate(slotX, slotY, false, true); card.scale.x = 0.01; uiLayer.addChild(card); // Replace the placeholder with the actual card object gameState.playerHand[i] = card; // Flip animation: expand card face tween(card, { scaleX: 1.0 }, { duration: 150, easing: tween.quadOut, onFinish: function onFinish() { LK.getSound('cardLand').play(); } }); } }); } }); }; for (var i = 0; i < cardsToDeal; i++) { (function (index) { LK.setTimeout(function () { dealCard(index); }, index * dealDelay); })(i); } // Enable deal button after all animations are scheduled to finish var totalAnimationTime = (cardsToDeal - 1) * dealDelay + 250 + 150 + 200; // Added buffer LK.setTimeout(function () { if (discardAreaContainer) { discardAreaContainer.interactive = true; updateUI(); } }, totalAnimationTime); } function createBurnEffect(x, y) { var numParticles = 4; // Slightly more particles for (var i = 0; i < numParticles; i++) { var particle = LK.getAsset('burnHeartMod', { anchorX: 0.5, anchorY: 0.5 }); particle.x = x + (Math.random() - 0.5) * 80; // Wider spread particle.y = y + (Math.random() - 0.5) * 80; particle.scale.set(0.3 + Math.random() * 0.2); // Adjusted for new asset var angle = Math.random() * Math.PI * 2; var distance = Math.random() * 40 + 25; // Larger movement distance var duration = 400 + Math.random() * 500; // Slightly longer duration var targetX = x + Math.cos(angle) * distance; var targetY = y + Math.sin(angle) * distance - Math.random() * 50; // Float upward more gameLayer.addChild(particle); tween(particle, { x: targetX, y: targetY, alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: duration, easing: tween.quadOut, onFinish: function (p) { return function () { if (p.parent) { p.parent.removeChild(p); } }; }(particle) }); } } function createIceSparkles(x, y) { var numSparkles = 2; for (var i = 0; i < numSparkles; i++) { var sparkle = LK.getAsset('iceCube', { anchorX: 0.5, anchorY: 0.5 }); sparkle.x = x + (Math.random() - 0.5) * 60; sparkle.y = y + (Math.random() - 0.5) * 60; sparkle.scale.set(0.1 + Math.random() * 0.1); sparkle.alpha = 0.6; sparkle.tint = 0x88ddff; // Light blue tint gameLayer.addChild(sparkle); tween(sparkle, { y: sparkle.y - 20, alpha: 0, scaleX: 0.05, scaleY: 0.05 }, { duration: 800, easing: tween.quadOut, onFinish: function (p) { return function () { if (p.parent) { p.parent.removeChild(p); } }; }(sparkle) }); } } function createIceShatterEffect(x, y) { var numShards = 6; for (var i = 0; i < numShards; i++) { var shard = LK.getAsset('iceCube', { anchorX: 0.5, anchorY: 0.5 }); shard.x = x; shard.y = y; shard.scale.set(0.2 + Math.random() * 0.2); shard.tint = 0xaaeeff; // Ice blue tint var angle = Math.PI * 2 * i / numShards + (Math.random() - 0.5) * 0.5; var distance = 40 + Math.random() * 30; var targetX = x + Math.cos(angle) * distance; var targetY = y + Math.sin(angle) * distance; gameLayer.addChild(shard); tween(shard, { x: targetX, y: targetY, alpha: 0, rotation: Math.random() * Math.PI * 2, scaleX: 0.05, scaleY: 0.05 }, { duration: 600, easing: tween.quadOut, onFinish: function (p) { return function () { if (p.parent) { p.parent.removeChild(p); } }; }(shard) }); } } function createModFloatAnimation(target, shadow) { var initialY = target.y; var floatDistance = 10; var baseDuration = 3000; var randomDurationOffset = 1500; var initialScale = shadow.scale.x; var minScale = initialScale * 0.7; var maxScale = initialScale * 1.0; function floatUp() { if (!target.parent || !target.parent.parent || !target.parent.parent.parent) { return; } var duration = baseDuration / 2 + Math.random() * randomDurationOffset; tween(target, { y: initialY - floatDistance }, { duration: duration, easing: tween.easeInOut, onFinish: floatDown }); tween(shadow, { scaleX: minScale, scaleY: minScale }, { duration: duration, easing: tween.easeInOut }); } function floatDown() { if (!target.parent || !target.parent.parent || !target.parent.parent.parent) { return; } var duration = baseDuration / 2 + Math.random() * randomDurationOffset; tween(target, { y: initialY + floatDistance }, { duration: duration, easing: tween.easeInOut, onFinish: floatUp }); tween(shadow, { scaleX: maxScale, scaleY: maxScale }, { duration: duration, easing: tween.easeInOut }); } LK.setTimeout(floatDown, Math.random() * baseDuration); } // Add this new function to calculate greed bonus function calculateGreedBonus(isPlayerSide, baseChipsEarned) { // Only calculate bonus if greed mod is equipped if (ModSystem.getEquippedModAsset('diamonds', isPlayerSide) !== 'chipsDiamondMod') { return 0; } var playArea = isPlayerSide ? gameState.playerPlayArea : gameState.aiPlayArea; var totalBonusPercentage = 0; // Calculate bonus from each diamond card on the board for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = playArea[row][col]; if (card && card.cardData.suit === 'diamonds') { // The "base gain" for this card is 5% var baseGain = 0.05; // Each level adds an additional 10% gain var levelGain = card.level * 0.1; totalBonusPercentage += baseGain + levelGain; } } } // Do not return fractional gains - round up return Math.ceil(baseChipsEarned * totalBonusPercentage); } function createStartScreen(fromMatch) { LK.playMusic('titleSong'); ModSystem.init(); // Clear any existing start screen elements startScreenElements.forEach(function (element) { if (element.parent) { element.parent.removeChild(element); } }); startScreenElements = []; var battleScreenContainer; // Add background animation var startScreenAnimContainer = new Container(); uiLayer.addChild(startScreenAnimContainer); startScreenElements.push(startScreenAnimContainer); var suitAssets = ['heartSuit', 'diamondSuit', 'clubSuit', 'spadeSuit']; var spacing = 400; var numCols = Math.ceil(SCREEN_WIDTH / spacing) + 3; var numRows = Math.ceil(SCREEN_HEIGHT / spacing) + 3; var patternWidth = numCols * spacing; var patternHeight = numRows * spacing; backgroundSuits = []; for (var row = 0; row < numRows; row++) { for (var col = 0; col < numCols; col++) { var suitIndex = (row + col) % suitAssets.length; var suitId = suitAssets[suitIndex]; var suit = LK.getAsset(suitId, { anchorX: 0.5, anchorY: 0.5 }); suit.x = col * spacing - spacing; suit.y = row * spacing - spacing; suit.alpha = 0.8; suit.scale.set(1.5); suit.baseX = col * spacing; suit.baseY = row * spacing; startScreenAnimContainer.addChild(suit); backgroundSuits.push(suit); } } var gameLogo = LK.getAsset('titleLogo', { anchorX: 0.5, anchorY: 0.5 }); gameLogo.x = SCREEN_WIDTH / 2; gameLogo.y = SCREEN_HEIGHT / 2 - 200; uiLayer.addChild(gameLogo); startScreenElements.push(gameLogo); var bottomBarContainer = new Container(); uiLayer.addChild(bottomBarContainer); startScreenElements.push(bottomBarContainer); function showBattleSelection() { gameLogo.visible = false; bottomBarContainer.visible = true; battleScreenContainer = new Container(); uiLayer.addChild(battleScreenContainer); startScreenElements.push(battleScreenContainer); var pvpIcon = LK.getAsset('pvpIcon', { anchorX: 0.5, anchorY: 0.5, interactive: true }); pvpIcon.scale.set(3); pvpIcon.x = SCREEN_WIDTH * 0.25; pvpIcon.y = SCREEN_HEIGHT * 0.45; battleScreenContainer.addChild(pvpIcon); var pvpButton = LK.getAsset('pvpButton', { anchorX: 0.5, anchorY: 0.5, interactive: true }); pvpButton.scale.set(3); pvpButton.x = pvpIcon.x; pvpButton.y = pvpIcon.y + pvpIcon.height / 2 + pvpButton.height / 2 + 150; battleScreenContainer.addChild(pvpButton); var battleRatingText = new Text2('Battle rating: ' + storage.battleRating, { size: 70, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 4 }); battleRatingText.anchor.set(0.5, 0.5); var battleRatingIcon = LK.getAsset('battleRatingIcon', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); var totalWidth = battleRatingIcon.width + 10 + battleRatingText.width; var yPos = pvpIcon.y + pvpIcon.height / 2 + 350; battleRatingIcon.x = pvpIcon.x - totalWidth / 2 + battleRatingIcon.width / 2; battleRatingIcon.y = yPos; battleRatingText.x = battleRatingIcon.x + battleRatingIcon.width / 2 + 10 + battleRatingText.width / 2; battleRatingText.y = yPos; battleScreenContainer.addChild(battleRatingIcon); battleScreenContainer.addChild(battleRatingText); var coopIcon = LK.getAsset('coopIcon', { anchorX: 0.5, anchorY: 0.5, interactive: true }); coopIcon.scale.set(3); coopIcon.x = SCREEN_WIDTH * 0.75; coopIcon.y = pvpIcon.y; battleScreenContainer.addChild(coopIcon); var coopButton = LK.getAsset('coopButton', { anchorX: 0.5, anchorY: 0.5, interactive: true }); coopButton.scale.set(3); coopButton.x = coopIcon.x; coopButton.y = coopIcon.y + coopIcon.height / 2 + coopButton.height / 2 + 150; battleScreenContainer.addChild(coopButton); var coopHighestWaveText = new Text2('Highest wave: ' + (storage.coopHighestWave || 0), { size: 70, fill: 0xffffff, weight: 800, stroke: 0x000000, strokeThickness: 4 }); coopHighestWaveText.anchor.set(0.5, 0.5); coopHighestWaveText.x = coopIcon.x; coopHighestWaveText.y = coopIcon.y + coopIcon.height / 2 + 350; battleScreenContainer.addChild(coopHighestWaveText); // === DEBUG BUTTON REMOVED === function onPlay(isTutorial, mode) { startGame(isTutorial, mode); } if (!storage.tutorialCompleted && fromMatch !== true) { TutorialSystem.start(); var items = TutorialSystem.startScreenItems; items.mods.button.interactive = false; items.mods.icon.interactive = false; items.shop.button.interactive = false; items.shop.icon.interactive = false; items.mods.button.alpha = 0.5; items.mods.icon.alpha = 0.5; items.shop.button.alpha = 0.5; items.shop.icon.alpha = 0.5; var overlay = new Container(); overlay.interactive = true; overlay.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.7; overlay.addChild(bg); battleScreenContainer.addChild(overlay); battleScreenContainer.removeChild(pvpIcon); battleScreenContainer.removeChild(pvpButton); overlay.addChild(pvpIcon); overlay.addChild(pvpButton); coopIcon.interactive = false; coopIcon.alpha = 0.3; coopButton.interactive = false; coopButton.alpha = 0.3; TutorialSystem.pulseAnimation(pvpIcon); TutorialSystem.pulseAnimation(pvpButton); var welcomeText = new Text2("Welcome to Double Down Defense! Let’s jump right into a game, tap on PvP to start!", { size: 70, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 5, align: 'center', wordWrap: true, wordWrapWidth: SCREEN_WIDTH - 200 }); welcomeText.anchor.set(0.5, 0.5); welcomeText.x = SCREEN_WIDTH / 2; welcomeText.y = SCREEN_HEIGHT * 0.25; overlay.addChild(welcomeText); pvpIcon.down = function () { onPlay(true, 'pvp'); }; pvpButton.down = function () { onPlay(true, 'pvp'); }; } else { pvpIcon.down = function () { onPlay(false, 'pvp'); }; pvpButton.down = function () { onPlay(false, 'pvp'); }; coopIcon.down = function () { onPlay(false, 'coop'); }; coopButton.down = function () { onPlay(false, 'coop'); }; } } if (fromMatch) { showBattleSelection(); } else { // Animate logo scaling in gameLogo.scale.set(0.01); bottomBarContainer.visible = false; tween(gameLogo, { scaleX: 1, scaleY: 1 }, { duration: 1200, easing: tween.elasticOut, onFinish: function onFinish() { var tapToStartText = new Text2('Tap anywhere to start', { size: 90, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 6 }); tapToStartText.anchor.set(0.5, 0.5); tapToStartText.x = SCREEN_WIDTH / 2; tapToStartText.y = gameLogo.y + gameLogo.height / 2 + 250; uiLayer.addChild(tapToStartText); startScreenElements.push(tapToStartText); function flashAnimation() { if (!tapToStartText.parent) { return; } tween(tapToStartText, { alpha: 0.2 }, { duration: 1200, easing: tween.easeInOut, onFinish: function onFinish() { if (!tapToStartText.parent) { return; } tween(tapToStartText, { alpha: 1.0 }, { duration: 1200, easing: tween.easeInOut, onFinish: flashAnimation }); } }); } flashAnimation(); var screenTapArea = new Container(); screenTapArea.interactive = true; screenTapArea.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); uiLayer.addChild(screenTapArea); startScreenElements.push(screenTapArea); screenTapArea.down = function () { tween.stop(tapToStartText); uiLayer.removeChild(tapToStartText); uiLayer.removeChild(screenTapArea); showBattleSelection(); }; } }); } var startBottomBar = LK.getAsset('bottomBar', { anchorX: 0.5, anchorY: 1 }); startBottomBar.x = SCREEN_WIDTH / 2; startBottomBar.y = SCREEN_HEIGHT; bottomBarContainer.addChild(startBottomBar); var topBar = LK.getAsset('bottomBar', { anchorX: 0.5, anchorY: 0 }); topBar.x = SCREEN_WIDTH / 2; topBar.y = 65; topBar.scale.y = 0.4; bottomBarContainer.addChild(topBar); var moneyText = new Text2('$' + storage.money, { size: 105, fill: 0xffd700, weight: 800, stroke: 0x000000, strokeThickness: 4 }); moneyText.anchor.set(1, 0.5); moneyText.x = SCREEN_WIDTH - 150; moneyText.y = topBar.y + topBar.height * topBar.scale.y / 2; bottomBarContainer.addChild(moneyText); // --- Title screen selection highlight asset --- var titleScreenSelection = LK.getAsset('titleScreenSelection', { anchorX: 0.5, anchorY: 1 }); titleScreenSelection.alpha = 0.3; bottomBarContainer.addChild(titleScreenSelection); // --- Track current selection: "battle", "mods", or "shop" --- var currentTitleScreenSelection = "battle"; // --- Calculate button positions for highlight --- var playButton = LK.getAsset('playButton', { anchorX: 0.5, anchorY: 0.5, interactive: true }); playButton.x = SCREEN_WIDTH / 2; playButton.y = SCREEN_HEIGHT - 100; // Add battleIcon directly above playButton var battleIcon = LK.getAsset('battleIcon', { anchorX: 0.5, anchorY: 1, interactive: true }); battleIcon.x = playButton.x; battleIcon.y = playButton.y - playButton.height / 2 - 10; // 30px gap above playButton bottomBarContainer.addChild(battleIcon); bottomBarContainer.addChild(playButton); // --- Suit mod button and mods icon --- var suitModButton = LK.getAsset('suitModButton', { anchorX: 0.5, anchorY: 0.5, interactive: true }); suitModButton.x = playButton.x + playButton.width / 2 + 100 + suitModButton.width / 2; suitModButton.y = playButton.y; // Add modsIcon directly above suitModButton, same orientation as battleIcon var modsIcon = LK.getAsset('modsIcon', { anchorX: 0.5, anchorY: 1, interactive: true }); modsIcon.x = suitModButton.x; modsIcon.y = suitModButton.y - suitModButton.height / 2 - 10; // 10px gap above suitModButton bottomBarContainer.addChild(modsIcon); bottomBarContainer.addChild(suitModButton); // --- Shop button and icon --- var shopButton = LK.getAsset('shopButton', { anchorX: 0.5, anchorY: 0.5, interactive: true }); shopButton.x = playButton.x - playButton.width / 2 - 100 - shopButton.width / 2; shopButton.y = playButton.y + 10; var shopIcon = LK.getAsset('shopIcon', { anchorX: 0.5, anchorY: 1, interactive: true }); shopIcon.x = shopButton.x; shopIcon.y = shopButton.y - shopButton.height / 2 - 10; bottomBarContainer.addChild(shopIcon); bottomBarContainer.addChild(shopButton); // --- Mods container as before --- var modsContainer = new Container(); modsContainer.visible = false; uiLayer.addChild(modsContainer); startScreenElements.push(modsContainer); // --- Shop Container --- var shopContainer = new Container(); shopContainer.visible = false; uiLayer.addChild(shopContainer); startScreenElements.push(shopContainer); var shopTitle = new Text2('Shop', { size: 150, fill: 0xffffff, weight: 'bold' }); shopTitle.anchor.set(0.5, 0.5); shopTitle.x = SCREEN_WIDTH / 2; shopTitle.y = 400; shopContainer.addChild(shopTitle); // Add black overlay behind the random mod shop item var shopOverlay = LK.getAsset('card', { anchorX: 0.5, anchorY: 0.5 }); var overlayWidth = 800; var overlayHeight = 1200; // 2:3 ratio shopOverlay.scale.set(overlayWidth / shopOverlay.width, overlayHeight / shopOverlay.height); shopOverlay.x = SCREEN_WIDTH / 2; shopOverlay.y = 1476 - 200; shopOverlay.tint = 0x000000; shopOverlay.alpha = 0.8; shopContainer.addChild(shopOverlay); // Add shopRandomMod asset in center var shopRandomModAsset = LK.getAsset('shopRandomMod', { anchorX: 0.5, anchorY: 0.5 }); shopRandomModAsset.x = SCREEN_WIDTH / 2; shopRandomModAsset.y = SCREEN_HEIGHT / 2 - 200; shopRandomModAsset.scale.set(0.8); shopContainer.addChild(shopRandomModAsset); // Add descriptive text underneath var shopDescText = new Text2('Buy a random card mod', { size: 60, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 3 }); shopDescText.anchor.set(0.5, 0.5); shopDescText.x = SCREEN_WIDTH / 2; shopDescText.y = shopRandomModAsset.y + shopRandomModAsset.height / 2 + 100; shopContainer.addChild(shopDescText); // Add price text var shopPriceText = new Text2('$' + storage.randomModPrice, { size: 80, fill: 0xffd700, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); shopPriceText.anchor.set(0.5, 0.5); shopPriceText.x = SCREEN_WIDTH / 2; shopPriceText.y = shopDescText.y + 80; shopContainer.addChild(shopPriceText); function buyRandomMod() { if (storage.money < storage.randomModPrice) { createFloatingText("Not enough money!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0xff0000, 100); return; } var unownedMods = ModSystem.getUnownedMods(); if (unownedMods.length === 0) { createFloatingText("All mods unlocked!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0x00ff00, 100); shopPriceText.setText('SOLD OUT'); shopOverlay.interactive = false; shopRandomModAsset.interactive = false; return; } storage.money -= storage.randomModPrice; moneyText.setText('$' + storage.money); shopOverlay.interactive = false; shopRandomModAsset.interactive = false; // Hide the static shop random mod asset and its text shopRandomModAsset.visible = false; shopDescText.visible = false; shopPriceText.visible = false; var slotContainer = new Container(); // Position it to replace the shopRandomModAsset slotContainer.x = shopRandomModAsset.x; slotContainer.y = shopRandomModAsset.y; shopContainer.addChild(slotContainer); var displayAsset = null; var spinDuration = 50; var spinsLeft = 20 + Math.floor(Math.random() * 15); // NEW LOGIC: Guarantee one of each suit before being fully random. // 1. Find which suits the player already owns at least one mod for. var ownedSuits = {}; for (var ownedModId in ModSystem.ownedMods) { if (ModSystem.ownedMods[ownedModId]) { // Check if the value is true var modData = ModSystem.modData[ownedModId]; if (modData) { ownedSuits[modData.suit] = true; } } } // 2. Identify suits for which the player owns no mods. var suitsWithNoMods = []; CardSystem.suits.forEach(function (suit) { if (!ownedSuits[suit]) { suitsWithNoMods.push(suit); } }); // 3. Create a pool of "priority" mods to choose from. var priorityUnownedMods = []; if (suitsWithNoMods.length > 0) { unownedMods.forEach(function (unownedModId) { var modData = ModSystem.modData[unownedModId]; if (modData && suitsWithNoMods.indexOf(modData.suit) !== -1) { priorityUnownedMods.push(unownedModId); } }); } var finalModId; // 4. Decide which pool to pick the final mod from. if (priorityUnownedMods.length > 0) { // The player is missing mods for some suits, so guarantee one from that pool. finalModId = priorityUnownedMods[Math.floor(Math.random() * priorityUnownedMods.length)]; } else { // The player either has at least one mod for every suit, or there are no unowned mods for the missing suits. // Fallback to picking any unowned mod. finalModId = unownedMods[Math.floor(Math.random() * unownedMods.length)]; } function spin() { spinsLeft--; if (displayAsset && displayAsset.parent) { displayAsset.parent.removeChild(displayAsset); } var modIdToShow = spinsLeft <= 0 ? finalModId : unownedMods[Math.floor(Math.random() * unownedMods.length)]; // Scale to fill the space of the shopRandomModAsset var sizingAsset = LK.getAsset('shopRandomMod', {}); var modAssetForSizing = LK.getAsset(modIdToShow, {}); // Match the visible height of the random mod asset, with a small margin var scaleToFit = sizingAsset.height * shopRandomModAsset.scale.y / modAssetForSizing.height * 0.9; displayAsset = LK.getAsset(modIdToShow, { anchorX: 0.5, anchorY: 0.5, scaleX: scaleToFit, scaleY: scaleToFit }); slotContainer.addChild(displayAsset); if (spinsLeft <= 0) { // REVEAL SEQUENCE ModSystem.unlockMod(finalModId); if (!TutorialSystem.isActive) { storage.randomModPrice = Math.floor(storage.randomModPrice * 1.5) + 50; } shopPriceText.setText('$' + storage.randomModPrice); // Create grey-out overlay var revealOverlay = new Container(); revealOverlay.interactive = true; revealOverlay.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); uiLayer.addChild(revealOverlay); var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.8; revealOverlay.addChild(bg); // Move the final mod asset to the reveal overlay var finalModAsset = displayAsset; finalModAsset.parent.removeChild(finalModAsset); revealOverlay.addChild(finalModAsset); finalModAsset.x = SCREEN_WIDTH / 2; finalModAsset.y = SCREEN_HEIGHT / 2; // The slot container is now empty, remove it. if (slotContainer.parent) { slotContainer.parent.removeChild(slotContainer); } // Grow larger animation tween(finalModAsset, { scaleX: finalModAsset.scale.x * 1.5, scaleY: finalModAsset.scale.y * 1.5 }, { duration: 500, easing: tween.elasticOut }); // "You received" message var modData = ModSystem.modData[finalModId]; var receivedText = new Text2("You received:\n" + modData.name, { size: 90, fill: 0xffd700, weight: 'bold', stroke: 0x000000, strokeThickness: 6, align: 'center' }); receivedText.anchor.set(0.5, 0.5); receivedText.x = SCREEN_WIDTH / 2; receivedText.y = finalModAsset.y + finalModAsset.height * finalModAsset.scale.y / 2 + 150; revealOverlay.addChild(receivedText); // Tap to continue message var tapToContinue = new Text2("Tap to continue", { size: 60, fill: 0xffffff, weight: 'bold' }); tapToContinue.anchor.set(0.5, 0.5); tapToContinue.x = SCREEN_WIDTH / 2; tapToContinue.y = SCREEN_HEIGHT - 200; revealOverlay.addChild(tapToContinue); // Set up tap to continue revealOverlay.down = function () { revealOverlay.interactive = false; // Prevent multiple taps // Fade out and remove reveal screen tween(revealOverlay, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (revealOverlay.parent) { revealOverlay.parent.removeChild(revealOverlay); } // Show shop elements again shopRandomModAsset.visible = true; shopDescText.visible = true; shopPriceText.visible = true; // Re-enable shop interaction if there are mods left if (ModSystem.getUnownedMods().length > 0) { shopOverlay.interactive = true; shopRandomModAsset.interactive = true; } else { shopPriceText.setText('SOLD OUT'); } } }); }; } else { if (spinsLeft < 10) { spinDuration += 25; } if (spinsLeft < 5) { spinDuration += 40; } LK.setTimeout(spin, spinDuration); } } spin(); } shopOverlay.interactive = true; shopOverlay.down = buyRandomMod; shopRandomModAsset.interactive = true; shopRandomModAsset.down = buyRandomMod; // Add pulsing animation to shopRandomMod function createPulseAnimation() { if (!shopRandomModAsset.parent) { return; } tween(shopRandomModAsset, { scaleX: 0.9, scaleY: 0.9 }, { duration: 1000, easing: tween.easeInOut, onFinish: function onFinish() { if (!shopRandomModAsset.parent) { return; } tween(shopRandomModAsset, { scaleX: 0.8, scaleY: 0.8 }, { duration: 1000, easing: tween.easeInOut, onFinish: createPulseAnimation }); } }); } createPulseAnimation(); // --- Selection highlight logic --- // Helper to move the highlight to the correct button function updateTitleScreenSelectionHighlight() { var targetX, targetY, targetW, targetH; if (currentTitleScreenSelection === "battle") { targetX = playButton.x; targetY = playButton.y + playButton.height / 2; // anchorY:1, so bottom edge targetW = playButton.width * 1.1; targetH = playButton.height + battleIcon.height + 20; // Cover button, icon, and gap with padding } else if (currentTitleScreenSelection === "mods") { targetX = suitModButton.x; targetY = suitModButton.y + suitModButton.height / 2; targetW = suitModButton.width * 1.1; targetH = suitModButton.height + modsIcon.height + 20; // Cover button, icon, and gap with padding } else if (currentTitleScreenSelection === "shop") { targetX = shopButton.x; targetY = shopButton.y + shopButton.height / 2; targetW = shopButton.width * 1.2; targetH = shopButton.height + shopIcon.height + 20; } // Animate movement and scaling for smoothness tween.stop(titleScreenSelection); tween(titleScreenSelection, { x: targetX, y: targetY, width: targetW, height: targetH }, { duration: 180, easing: tween.cubicOut }); } // Set initial size and position for highlight (battle by default) titleScreenSelection.width = playButton.width * 1.2; titleScreenSelection.height = playButton.height + battleIcon.height + 20; titleScreenSelection.x = playButton.x; titleScreenSelection.y = playButton.y + playButton.height / 2; // --- Selection switching logic --- // Screens: "battle", "mods", "shop" playButton.down = function () { if (currentTitleScreenSelection !== "battle") { currentTitleScreenSelection = "battle"; updateTitleScreenSelectionHighlight(); } // Show battle screen, hide mods, shop and logo modsContainer.visible = false; shopContainer.visible = false; if (battleScreenContainer) { battleScreenContainer.visible = true; } gameLogo.visible = false; }; suitModButton.down = function () { if (currentTitleScreenSelection === "mods") { return; } if (currentTitleScreenSelection !== "mods") { currentTitleScreenSelection = "mods"; updateTitleScreenSelectionHighlight(); } // Show mods, hide others modsContainer.visible = true; shopContainer.visible = false; if (battleScreenContainer) { battleScreenContainer.visible = false; } gameLogo.visible = false; if (modsContainer.visible) { ModSystem.updateTopSuitDisplay(); // Refresh mod grid visuals var gridContainer = modsContainer.children[modsContainer.children.length - 1]; if (gridContainer && gridContainer.children.length > 0) { var suitModAssets = ['burnHeartMod', 'chipsDiamondMod', 'spreadClubMod', 'freezeSpadeMod', 'unityHeartsMod', 'gamblerDiamondsMod', 'slowClubsMod', 'mineSpadesMod', 'investmentHeartsMod', 'boostDiamondsMod', 'ricochetClubsMod', 'deathSpadesMod']; var childrenPerMod = 4; // Assumes cell, shadow, mod asset, name text for (var modIndex = 0; modIndex < suitModAssets.length; modIndex++) { var assetId = suitModAssets[modIndex]; var isOwned = ModSystem.isModOwned(assetId); var cellIndex = modIndex * childrenPerMod; var modAssetIndex = cellIndex + 2; if (gridContainer.children[cellIndex] && gridContainer.children[modAssetIndex]) { var cell = gridContainer.children[cellIndex]; var modAsset = gridContainer.children[modAssetIndex]; if (isOwned) { modAsset.tint = 0xffffff; modAsset.alpha = 1.0; cell.tint = 0xffffff; } else { modAsset.tint = 0x555555; modAsset.alpha = 0.6; cell.tint = 0x888888; } } } } } }; shopButton.down = function () { if (currentTitleScreenSelection === "shop") { return; } if (currentTitleScreenSelection !== "shop") { currentTitleScreenSelection = "shop"; updateTitleScreenSelectionHighlight(); } // Show shop, hide others shopContainer.visible = true; modsContainer.visible = false; if (battleScreenContainer) { battleScreenContainer.visible = false; } gameLogo.visible = false; }; battleIcon.down = playButton.down; modsIcon.down = suitModButton.down; var originalPlayDown = playButton.down; var originalModsDown = suitModButton.down; var originalShopDown = shopButton.down; playButton.down = originalPlayDown; suitModButton.down = originalModsDown; shopButton.down = originalShopDown; battleIcon.down = playButton.down; modsIcon.down = suitModButton.down; shopIcon.down = shopButton.down; var shopItemsForTutorial = { overlay: shopOverlay, asset: shopRandomModAsset, buyButtonDown: buyRandomMod, priceText: shopPriceText }; TutorialSystem.registerStartScreenElements({ battle: { button: playButton, icon: battleIcon, down: originalPlayDown }, mods: { button: suitModButton, icon: modsIcon, down: originalModsDown }, shop: { button: shopButton, icon: shopIcon, down: originalShopDown }, modsContainer: modsContainer, shopContainer: shopContainer, shopItems: shopItemsForTutorial }); if (TutorialSystem.isActive && fromMatch) { // A small delay to ensure the start screen elements are fully rendered LK.setTimeout(function () { TutorialSystem.advanceStep(); }, 200); } var numCircles = suitAssets.length; var totalBarWidth = SCREEN_WIDTH - 200; var suitSpacing = totalBarWidth / numCircles; var startX = SCREEN_WIDTH / 2 - totalBarWidth / 2 + suitSpacing / 2; var yOffset = SCREEN_HEIGHT * 0.1; var circleY = 300 + yOffset; ModSystem.topSuitGraphics = []; // Reset the array for (var i = 0; i < numCircles; i++) { var circleX = startX + i * suitSpacing; var suitContainer = new Container(); var circleBg = LK.getAsset('card', { anchorX: 0.5, anchorY: 0.5 }); circleBg.scale.set(1.5); suitContainer.addChild(circleBg); suitContainer.x = circleX; suitContainer.y = circleY - SCREEN_HEIGHT * 0.05 + 100; modsContainer.addChild(suitContainer); ModSystem.topSuitGraphics.push(suitContainer); } // --- Dark overlay below the four cards and above the equipped mods text --- var overlayCard = LK.getAsset('card', { anchorX: 0.5, anchorY: 0 }); var overlayWidth = totalBarWidth + 120; // Wide enough for all four cards and some margin var overlayHeight = 650; // Tall enough to cover the cards and the equipped mods text overlayCard.scale.set(overlayWidth / overlayCard.width, overlayHeight / overlayCard.height); overlayCard.x = SCREEN_WIDTH / 2; overlayCard.y = circleY - SCREEN_HEIGHT * 0.05 - overlayCard.height * overlayCard.scale.y / 2 + 130; // Start just above the cards overlayCard.alpha = 0.6; overlayCard.tint = 0x000000; modsContainer.addChildAt(overlayCard, 0); var equippedModsText = new Text2('Currently equipped mods', { size: 45, fill: 0xffffff, weight: '600' }); equippedModsText.anchor.set(0.5, 0); equippedModsText.x = SCREEN_WIDTH / 2; equippedModsText.y = circleY - SCREEN_HEIGHT * 0.05 + 350; // Position 25px below the mod icons modsContainer.addChild(equippedModsText); var gridContainer = new Container(); gridContainer.x = SCREEN_WIDTH / 2; gridContainer.y = circleY + 450 - SCREEN_HEIGHT * 0.05; modsContainer.addChild(gridContainer); var cols = 4; var rows = 3; var cardForSizing = LK.getAsset('card', {}); var cellWidth = cardForSizing.width; var cellHeight = cardForSizing.height; var colSpacing = 160; var rowSpacing = 140; var gridTotalWidth = cols * cellWidth + (cols - 1) * colSpacing; var gridStartX = -gridTotalWidth / 2; var gridStartY = 50; var suitModAssets = ['burnHeartMod', 'chipsDiamondMod', 'spreadClubMod', 'freezeSpadeMod', 'unityHeartsMod', 'gamblerDiamondsMod', 'slowClubsMod', 'mineSpadesMod', 'investmentHeartsMod', 'boostDiamondsMod', 'ricochetClubsMod', 'deathSpadesMod']; for (var r = 0; r < rows; r++) { for (var c = 0; c < cols; c++) { var cell = LK.getAsset('card', { anchorX: 0, anchorY: 0 }); cell.scale.set(1.3); cell.x = gridStartX + c * (cellWidth + colSpacing); cell.y = gridStartY + r * (cellHeight + rowSpacing); gridContainer.addChild(cell); var modIndex = r * cols + c; if (modIndex < suitModAssets.length) { var modAssetId = suitModAssets[modIndex]; if (modAssetId) { var shadow = LK.getAsset('suitShadow', { anchorX: 0.5, anchorY: 0.5 }); shadow.x = cell.x + cell.width * cell.scale.x / 2; shadow.y = cell.y + cell.height * cell.scale.y - cell.height * cell.scale.y / 5 + 15; shadow.alpha = 0.3; gridContainer.addChild(shadow); var modAsset = LK.getAsset(modAssetId, { anchorX: 0.5, anchorY: 0.5 }); modAsset.x = cell.x + cell.width * cell.scale.x / 2; modAsset.y = cell.y + cell.height * cell.scale.y / 2; var isOwned = ModSystem.isModOwned(modAssetId); if (!isOwned) { modAsset.tint = 0x555555; modAsset.alpha = 0.6; cell.tint = 0x888888; } gridContainer.addChild(modAsset); var modData = ModSystem.modData[modAssetId]; if (modData) { var nameText = new Text2(modData.name.toUpperCase(), { size: 35, fill: 0x000000, weight: 'bold', strokeThickness: 0 }); nameText.anchor.set(0.5, 0); nameText.x = cell.x + cell.width * cell.scale.x / 2; nameText.y = cell.y + 20; gridContainer.addChild(nameText); } createModFloatAnimation(modAsset, shadow); // Add click handler for mod display to the entire cell card cell.interactive = true; (function (assetId, modAssetRef) { cell.down = function () { ModSystem.showModDisplay(assetId, modAssetRef.x, modAssetRef.y); }; })(modAssetId, modAsset); } } } } currentGameState = 'start'; } function startGame(isTutorial, mode) { gameMode = mode || 'pvp'; LK.stopMusic(); // Clear start screen elements startScreenElements.forEach(function (element) { if (element.parent) { element.parent.removeChild(element); } }); startScreenElements = []; backgroundSuits = []; // Show game elements gameElements.forEach(function (element) { element.visible = true; }); if (gameMode === 'coop') { background.visible = false; coopBackground.visible = true; } else { background.visible = true; coopBackground.visible = false; } currentGameState = 'playing'; initializeGame(isTutorial || false); } function initializeGame(isTutorial) { // Disable deal button at the start of the round discardAreaContainer.interactive = false; discardAreaGraphic.tint = 0x666666; discardAreaGraphic.alpha = 1.0; ModSystem.init(); AISystem.setupMods(); PoolManager.init(); PathSystem.init(); // Initialize play areas gameState.playerPlayArea = []; gameState.aiPlayArea = []; playerHandNameTexts = [null, null]; for (var row = 0; row < PLAY_AREA_ROWS; row++) { gameState.playerPlayArea[row] = []; gameState.aiPlayArea[row] = []; for (var col = 0; col < PLAY_AREA_COLS; col++) { gameState.playerPlayArea[row][col] = null; gameState.aiPlayArea[row][col] = null; } } // Create initial decks gameState.playerDeck = CardSystem.createDeck(); gameState.aiDeck = CardSystem.createDeck(); // Draw grid lines drawPlayAreas(); drawPaths(); createLifeDisplays(); lastPlayerLives = gameState.playerLives; if (gameMode === 'coop') { gameState.aiLives = gameState.playerLives; playerTotalDamage = 0; aiTotalDamage = 0; } lastAiLives = gameState.aiLives; // Initialize player's hand with empty slots gameState.playerHand = [null, null, null, null, null]; // Center bar removed if (isTutorial) { LK.playMusic('pvpMusic'); gameReady = false; // Don't start waves automatically AISystem.thinkDelay = 999999; // Effectively disable AI TutorialSystem.startInGameTutorial(); } else { // Start the wave system with a "Get Ready" message var getReadyText = new Text2('Get ready…', { size: 150, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 10 }); getReadyText.anchor.set(0.5, 0.5); getReadyText.x = SCREEN_WIDTH / 2; getReadyText.y = SCREEN_HEIGHT / 2 - 155; getReadyText.alpha = 0; uiLayer.addChild(getReadyText); tween(getReadyText, { alpha: 1 }, { duration: 500, onFinish: function onFinish() { LK.getSound('getReady').play(); } }); gameReady = false; // After 2 seconds, display "Deal 'em!" and begin the first wave. LK.setTimeout(function () { if (!getReadyText.parent) { return; } getReadyText.setText('Deal \'em!'); LK.getSound('gameStart').play(); // Deal initial hand dealInitialHand(); // Remove the message after it has been displayed for a short time LK.setTimeout(function () { if (getReadyText.parent) { tween(getReadyText, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { if (getReadyText.parent) { getReadyText.parent.removeChild(getReadyText); } gameReady = true; LK.playMusic('pvpMusic'); } }); } }, 1000); }, 2000); } } function createLifeDisplays() { // Clear any existing hearts and labels if (opponentNameText && opponentNameText.parent) { opponentNameText.parent.removeChild(opponentNameText); } if (playerNameText && playerNameText.parent) { playerNameText.parent.removeChild(playerNameText); } playerLifeHearts.forEach(function (h) { if (h.parent) { h.parent.removeChild(h); } }); playerLifeHearts = []; aiLifeHearts.forEach(function (h) { if (h.parent) { h.parent.removeChild(h); } }); aiLifeHearts = []; if (gameMode === 'coop') { var heartSpacing = 110; var heartScale = 0.5; // Position to the right of the coop background, adjusted up and left var startX = coopBackground.x + coopBackground.width / 2 + 80 - SCREEN_WIDTH * 0.05; var startY = SCREEN_HEIGHT / 2 - heartSpacing - SCREEN_HEIGHT * 0.07; // Player lives are unified in coop mode for (var i = 0; i < gameState.playerLives; i++) { var heart = LK.getAsset('heartSuit', { anchorX: 0.5, anchorY: 0.5, scaleX: heartScale, scaleY: heartScale }); heart.x = startX; heart.y = startY + i * heartSpacing; uiLayer.addChild(heart); playerLifeHearts.push(heart); } } else { var heartSpacing = 110; var heartScale = 0.5; var startX_AI = 200 - 70; var startX_Player = SCREEN_WIDTH - 200 + 70; var yPos = SCREEN_HEIGHT / 2 - 137 + 10; var labelYPos = yPos - 60; // Opponent Name Text var totalAIHeartsWidth = (gameState.aiLives - 1) * heartSpacing; opponentNameText = new Text2('Opponent', { size: 40, fill: 0xffffff, weight: 800 }); opponentNameText.anchor.set(0.5, 1); opponentNameText.x = startX_AI + totalAIHeartsWidth / 2; opponentNameText.y = labelYPos; uiLayer.addChild(opponentNameText); // Player Name Text var totalPlayerHeartsWidth = (gameState.playerLives - 1) * heartSpacing; playerNameText = new Text2('Player', { size: 40, fill: 0xffffff, weight: 800 }); playerNameText.anchor.set(0.5, 1); playerNameText.x = startX_Player - totalPlayerHeartsWidth / 2; playerNameText.y = labelYPos; uiLayer.addChild(playerNameText); // AI Lives (left side) for (var i = 0; i < gameState.aiLives; i++) { var heart = LK.getAsset('heartSuit', { anchorX: 0.5, anchorY: 0.5, scaleX: heartScale, scaleY: heartScale }); heart.x = startX_AI + i * heartSpacing; heart.y = yPos; uiLayer.addChild(heart); aiLifeHearts.push(heart); } // Player Lives (right side) for (var i = 0; i < gameState.playerLives; i++) { var heart = LK.getAsset('heartSuit', { anchorX: 0.5, anchorY: 0.5, scaleX: heartScale, scaleY: heartScale }); heart.x = startX_Player - i * heartSpacing; heart.y = yPos; uiLayer.addChild(heart); playerLifeHearts.push(heart); } } } function dealNewHand() { if (gameState.playerChips < gameState.dealCost) { return; } // Find an empty slot in the player's hand var emptySlotIndex = -1; for (var i = 0; i < gameState.playerHand.length; i++) { if (!gameState.playerHand[i]) { emptySlotIndex = i; break; } } // If hand is full, do nothing if (emptySlotIndex === -1) { return; } gameState.playerChips -= gameState.dealCost; gameState.dealCount++; // Progressive cost increase with diminishing returns var baseCost = 25; // Changed from 38 to 25 var newCost; if (gameState.dealCount <= 10) { // First 10 deals: 8% increase each time newCost = Math.floor(baseCost * Math.pow(1.08, gameState.dealCount)); } else if (gameState.dealCount <= 25) { // Deals 11-25: 5% increase each time var cost10 = Math.floor(baseCost * Math.pow(1.08, 10)); // Cost at deal 10 var additionalDeals = gameState.dealCount - 10; newCost = Math.floor(cost10 * Math.pow(1.05, additionalDeals)); } else { // Deals 26+: 3% increase each time var cost10 = Math.floor(baseCost * Math.pow(1.08, 10)); var cost25 = Math.floor(cost10 * Math.pow(1.05, 15)); // 15 deals at 5% var additionalDeals = gameState.dealCount - 25; newCost = Math.floor(cost25 * Math.pow(1.03, additionalDeals)); } gameState.dealCost = newCost; if (gameState.playerDeck.length === 0) { gameState.playerDeck = CardSystem.createDeck(); } var cardData = gameState.playerDeck.pop(); // After every 10 waves, new cards dealt start one level higher var startLevel = Math.floor((WaveSystem.waveNumber - 1) / 10) + 1; var i = emptySlotIndex; var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px var handStartX = (SCREEN_WIDTH - handWidth) / 2; var slotX = handStartX + i * DEAL_SLOT_WIDTH + i * 30 + DEAL_SLOT_WIDTH / 2; var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2; // Start position - off to the bottom var startX = slotX + (Math.random() - 0.5) * 50; // Slight horizontal variance var startY = SCREEN_HEIGHT + DEAL_SLOT_HEIGHT; // Mark slot as occupied immediately to prevent overwriting if deal is pressed too quickly gameState.playerHand[i] = true; var cardBack = LK.getAsset('cardBack', { anchorX: 0.5, anchorY: 0.5 }); cardBack.x = startX; cardBack.y = startY; uiLayer.addChild(cardBack); // Animate card flying into place and flipping tween(cardBack, { x: slotX, y: slotY }, { duration: 250, easing: tween.cubicOut, onFinish: function onFinish() { // Flip animation: shrink back tween(cardBack, { scaleX: 0.01 }, { duration: 150, easing: tween.quadIn, onFinish: function onFinish() { if (cardBack.parent) { cardBack.parent.removeChild(cardBack); } // Create and show the real card var card = new Card(cardData); card.setLevel(startLevel); card.activate(slotX, slotY, false, true); card.scale.x = 0.01; uiLayer.addChild(card); // Replace the placeholder with the actual card object gameState.playerHand[i] = card; // Flip animation: expand card face tween(card, { scaleX: 1.0 }, { duration: 150, easing: tween.quadOut, onFinish: function onFinish() { LK.getSound('cardLand').play(); } }); } }); } }); updateUI(); } function updateUI() { playerChipsText.setText('Chips: ' + formatNumberWithSuffix(gameState.playerChips)); // Update combined deal/discard button text and appearance when not dragging if (!isDragging) { discardText.setText('-' + formatNumberWithSuffix(gameState.dealCost)); discardText.fill = 0xffffff; // Update button color based on affordability if (gameState.playerChips >= gameState.dealCost) { discardAreaGraphic.tint = 0xffffff; // No tint when affordable discardAreaGraphic.alpha = 1.0; // Full alpha when affordable } else { discardAreaGraphic.tint = 0x666666; // Grey tint when not affordable discardAreaGraphic.alpha = 1.0; // Keep full alpha even when not affordable } } waveText.setText('Wave: ' + WaveSystem.waveNumber); } function drawPlayAreas() { // Draw player play area slots var hand1Text = 'HAND1'; var hand2Text = 'HAND2'; for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { var slot = new Container(); var slotGraphics = slot.attachAsset('card', { anchorX: 0.5, anchorY: 0.5 }); slotGraphics.alpha = 0.5; var textToShow = row === 0 ? hand1Text : hand2Text; var text = new Text2(textToShow.charAt(col), { size: 180, fill: 0xffffff, weight: 'bold' }); text.anchor.set(0.5); text.alpha = 0.3; slot.addChild(text); slot.x = PLAYER_AREA_X + col * SLOT_WIDTH + SLOT_WIDTH / 2; slot.y = PLAYER_AREA_Y + row * SLOT_HEIGHT + SLOT_HEIGHT / 2; gameLayer.addChild(slot); slotIndicators.push(slot); } } // Draw AI play area slots for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { var slot = new Container(); var slotGraphics = slot.attachAsset('card', { anchorX: 0.5, anchorY: 0.5 }); slotGraphics.alpha = 0.5; slotGraphics.tint = 0xff8888; // Red tint for AI area var textToShow = row === 0 ? hand1Text : hand2Text; var text = new Text2(textToShow.charAt(col), { size: 180, fill: 0xffffff, weight: 'bold' }); text.anchor.set(0.5); text.alpha = 0.3; slot.addChild(text); slot.x = AI_AREA_X + col * SLOT_WIDTH + SLOT_WIDTH / 2; slot.y = AI_AREA_Y + row * SLOT_HEIGHT + SLOT_HEIGHT / 2; gameLayer.addChild(slot); slotIndicators.push(slot); } } // Draw player deal area slots (hand) var rating = storage.battleRating; var ratingString = String(rating); while (ratingString.length < 4) { ratingString = '0' + ratingString; } for (var i = 0; i < 5; i++) { var dealSlot = new Container(); var dealSlotGraphics = dealSlot.attachAsset('dealSlot', { anchorX: 0.5, anchorY: 0.5 }); dealSlotGraphics.alpha = 0.5; if (i === 0) { var icon = dealSlot.attachAsset('battleRatingIcon', { anchorX: 0.5, anchorY: 0.5 }); icon.alpha = 0.3; var iconScale = dealSlotGraphics.width * 0.8 / icon.width; icon.scale.set(iconScale); } else { var digit = ratingString.charAt(i - 1); var text = new Text2(digit, { size: 180, fill: 0xffffff, weight: 'bold' }); text.anchor.set(0.5); text.alpha = 0.3; dealSlot.addChild(text); } var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px var handStartX = (SCREEN_WIDTH - handWidth) / 2; dealSlot.x = handStartX + i * DEAL_SLOT_WIDTH + i * 30 + DEAL_SLOT_WIDTH / 2; dealSlot.y = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2; gameLayer.addChild(dealSlot); slotIndicators.push(dealSlot); } } function drawPaths() { // Path lines removed - enemies still follow invisible paths } // drawPathSegment function removed - no longer needed since path lines are removed function getSlotPosition(row, col, isPlayerArea) { var baseX = isPlayerArea ? PLAYER_AREA_X : AI_AREA_X; var baseY = isPlayerArea ? PLAYER_AREA_Y : AI_AREA_Y; return { x: baseX + col * SLOT_WIDTH + SLOT_WIDTH / 2, y: baseY + row * SLOT_HEIGHT + SLOT_HEIGHT / 2 }; } function getSlotFromPosition(x, y) { // Check player play area if (x >= PLAYER_AREA_X && x <= PLAYER_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH && y >= PLAYER_AREA_Y && y <= PLAYER_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT) { var col = Math.floor((x - PLAYER_AREA_X) / SLOT_WIDTH); var row = Math.floor((y - PLAYER_AREA_Y) / SLOT_HEIGHT); if (col >= 0 && col < PLAY_AREA_COLS && row >= 0 && row < PLAY_AREA_ROWS) { return { area: 'player', row: row, col: col }; } } // Check player hand area if (y >= PLAYER_DEAL_AREA_Y && y <= PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT) { var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px var handStartX = (SCREEN_WIDTH - handWidth) / 2; for (var i = 0; i < 5; i++) { var slotXStart = handStartX + i * (DEAL_SLOT_WIDTH + 30); var slotXEnd = slotXStart + DEAL_SLOT_WIDTH; if (x >= slotXStart && x <= slotXEnd) { return { area: 'hand', index: i }; } } } // Check discard area if (discardAreaContainer && x >= discardAreaContainer.x - DEAL_SLOT_WIDTH / 2 && x <= discardAreaContainer.x + DEAL_SLOT_WIDTH / 2 && y >= discardAreaContainer.y - DEAL_SLOT_HEIGHT / 2 && y <= discardAreaContainer.y + DEAL_SLOT_HEIGHT / 2) { return { area: 'discard' }; } return null; } function evaluateRowHand(row, isPlayerArea) { var playArea = isPlayerArea ? gameState.playerPlayArea : gameState.aiPlayArea; var cards = []; for (var col = 0; col < PLAY_AREA_COLS; col++) { if (playArea[row][col]) { cards.push(playArea[row][col]); } } return CardSystem.evaluatePokerHand(cards); } function updateHandNameDisplay(row, handEval) { var existingText = playerHandNameTexts[row]; var shouldShowText = handEval.strength > 1; if (shouldShowText) { var handName = handEval.type.replace(/_/g, ' ').toUpperCase(); if (existingText) { if (existingText.text !== handName) { existingText.setText(handName); } existingText.visible = true; } else { var newText = new Text2(handName, { size: 50, fill: 0xffffff, weight: '800', stroke: 0x000000, strokeThickness: 0 }); newText.anchor.set(0.5, 0); newText.alpha = 0.8; newText.x = PLAYER_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH / 2; newText.y = PLAYER_AREA_Y + (row + 1) * SLOT_HEIGHT - 20; uiLayer.addChild(newText); playerHandNameTexts[row] = newText; } } else { if (existingText) { existingText.visible = false; } } } function applyHandBonuses() { // Apply bonuses to player cards for (var row = 0; row < PLAY_AREA_ROWS; row++) { var handEval = evaluateRowHand(row, true); updateHandNameDisplay(row, handEval); var contributingCards = handEval.contributingCards || []; for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = gameState.playerPlayArea[row][col]; if (card) { card.handBonus = handEval.multiplier; card.calculateStats(); card.redOutline.visible = handEval.strength > 1 && contributingCards.indexOf(card) !== -1; } } } // Apply bonuses to AI cards AISystem.applyAIHandBonuses(); } function getAIDifficultySettings(battleRating) { // Post-tutorial AI intelligence now scales more aggressively with battle rating. // Normalize rating from 0 to 1, clamped between 0 and a max rating of 1500 (was 2000). var maxRatingForScaling = 1500; var normalizedRating = Math.max(0, Math.min(battleRating || 0, maxRatingForScaling)) / maxRatingForScaling; // thinkDelay: from 300 (5s) down to 45 (0.75s). Starts faster than the original 360 (6s). var thinkDelay = 300 - normalizedRating * (300 - 45); // optimizationChance: from 10% up to 80%. Higher baseline and ceiling (was 5%-70%). var optimizationChance = 0.1 + normalizedRating * (0.8 - 0.1); // shouldOptimize: enabled above 50 rating (was 100). var shouldOptimize = (battleRating || 0) > 50; return { thinkDelay: Math.floor(thinkDelay), optimizationChance: optimizationChance, shouldOptimize: shouldOptimize, canLevelUp: (battleRating || 0) > 0 }; } /**** * AI System ****/ var AISystem = { thinkTimer: 0, update: function update() { var difficulty = getAIDifficultySettings(storage.battleRating); var currentThinkDelay = TutorialSystem.isActive ? 240 : difficulty.thinkDelay; this.thinkTimer++; if (this.thinkTimer >= currentThinkDelay) { this.thinkTimer = 0; this.makeMove(); } }, shouldDeal: function shouldDeal() { // Deal if we can afford it and have empty slots if (gameState.aiChips < gameState.dealCost) { return false; } var emptySlots = this.countEmptySlots(); // Deal if there are any empty slots at all return emptySlots > 0; }, countEmptySlots: function countEmptySlots() { var count = 0; for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { if (!gameState.aiPlayArea[row][col]) { count++; } } } return count; }, countLowLevelCards: function countLowLevelCards() { var count = 0; for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = gameState.aiPlayArea[row][col]; if (card && card.level <= 2) { count++; } } } return count; }, makeMove: function makeMove() { var difficulty = getAIDifficultySettings(storage.battleRating); // Priority order: Merge > Place > Optimize > Deal. Only one action per cycle. if (difficulty.canLevelUp && this.tryMergeCards()) { // Successfully merged, wait for next think cycle return; } if (this.tryPlaceCards()) { // Successfully placed cards return; } if (difficulty.shouldOptimize && this.optimizeCardPositions()) { // Successfully optimized, wait for next think cycle return; } // As a last resort, try to deal a new card if possible if (this.shouldDeal()) { this.dealAIHand(); } }, tryMergeCards: function tryMergeCards() { // Look for mergeable cards for (var row1 = 0; row1 < PLAY_AREA_ROWS; row1++) { for (var col1 = 0; col1 < PLAY_AREA_COLS; col1++) { var card1 = gameState.aiPlayArea[row1][col1]; if (!card1) { continue; } // Look for a card to merge with for (var row2 = 0; row2 < PLAY_AREA_ROWS; row2++) { for (var col2 = 0; col2 < PLAY_AREA_COLS; col2++) { if (row1 === row2 && col1 === col2) { continue; } var card2 = gameState.aiPlayArea[row2][col2]; if (!card2) { continue; } if (card1.canMergeWith(card2)) { this.mergeCards(card1, card2, row1, col1, row2, col2); return true; } } } } } return false; }, mergeCards: function mergeCards(card1, card2, row1, col1, row2, col2) { var mergedCard = card1.mergeWith(card2); if (mergedCard) { var oldLevel = card2.level; // Remove old cards gameLayer.removeChild(card1); gameLayer.removeChild(card2); gameState.aiPlayArea[row1][col1] = null; gameState.aiPlayArea[row2][col2] = null; // Place merged card in the first position var pos = getSlotPosition(row1, col1, false); mergedCard.activate(pos.x, pos.y, true, false); gameLayer.addChild(mergedCard); gameState.aiPlayArea[row1][col1] = mergedCard; // Merge animation mergedCard.alpha = 0; mergedCard.scaleX = mergedCard.scaleY = 1.5; tween(mergedCard, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.elasticOut }); createFloatingText('+Level Up!', mergedCard.x, mergedCard.y - 50, 0x00ff00, 60); var damageBefore = Math.floor(10 * Math.pow(1.6, oldLevel - 1)); var fireRateBefore = Math.max(15, Math.floor(60 / Math.pow(1.2, oldLevel - 1))); var damageAfter = Math.floor(10 * Math.pow(1.6, mergedCard.level - 1)); var fireRateAfter = Math.max(15, Math.floor(60 / Math.pow(1.2, mergedCard.level - 1))); var damageIncrease = damageBefore > 0 ? Math.round((damageAfter / damageBefore - 1) * 100) : 0; var fireRateIncrease = fireRateAfter > 0 && fireRateAfter < fireRateBefore ? Math.round((fireRateBefore / fireRateAfter - 1) * 100) : 0; var statText = ""; if (damageBefore > 0) { var dmgMult = (damageAfter / damageBefore).toFixed(2); statText = "Dmg x" + dmgMult; } createFloatingText(statText, mergedCard.x, mergedCard.y + 55, 0x00ff00, 35); this.applyAIHandBonuses(); } }, tryPlaceCards: function tryPlaceCards() { // AI doesn't have a "hand" like player, it deals directly to board // This function is for future expansion return false; }, optimizeCardPositions: function optimizeCardPositions() { // Move stronger cards to better positions (like completing poker hands) // For now, just try to complete rows for hand bonuses return this.tryCompletePokerHands(); }, tryCompletePokerHands: function tryCompletePokerHands() { for (var row = 0; row < PLAY_AREA_ROWS; row++) { var rowCards = []; var emptyPositions = []; for (var col = 0; col < PLAY_AREA_COLS; col++) { if (gameState.aiPlayArea[row][col]) { rowCards.push({ card: gameState.aiPlayArea[row][col], col: col }); } else { emptyPositions.push(col); } } // If row is almost complete, try to fill it strategically if (rowCards.length >= 3 && emptyPositions.length > 0) { // Look for cards in other rows that might complete a hand if (this.tryMoveCardToCompleteHand(row, rowCards, emptyPositions[0])) { return true; // A move was made, so we are done for this 'think' cycle } } } return false; // No move was made }, tryMoveCardToCompleteHand: function tryMoveCardToCompleteHand(targetRow, existingCards, targetCol) { var difficulty = getAIDifficultySettings(storage.battleRating); // Look for cards in other positions that might help complete a hand for (var row = 0; row < PLAY_AREA_ROWS; row++) { if (row === targetRow) { continue; } for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = gameState.aiPlayArea[row][col]; if (!card) { continue; } // Simple heuristic: move cards of same suit or sequential values var shouldMove = this.cardHelpsHand(card, existingCards); if (shouldMove && Math.random() < difficulty.optimizationChance) { // Dynamic chance to move // Move the card gameState.aiPlayArea[row][col] = null; gameState.aiPlayArea[targetRow][targetCol] = card; var newPos = getSlotPosition(targetRow, targetCol, false); tween(card, { x: newPos.x, y: newPos.y }, { duration: 300, easing: tween.quadOut }); this.applyAIHandBonuses(); return true; } } } return false; }, cardHelpsHand: function cardHelpsHand(card, existingCards) { // Simple heuristic to see if a card might help complete a poker hand var suits = {}; var values = {}; existingCards.forEach(function (cardInfo) { var c = cardInfo.card.cardData; suits[c.suit] = (suits[c.suit] || 0) + 1; values[c.value] = (values[c.value] || 0) + 1; }); // Check if card matches existing suits or values var cardSuit = card.cardData.suit; var cardValue = card.cardData.value; return suits[cardSuit] >= 2 || values[cardValue] >= 1; }, dealAIHand: function dealAIHand() { if (gameState.aiChips < gameState.dealCost) { return; } gameState.aiChips -= gameState.dealCost; // Deal one card to an empty slot var cardData; // AI cannot play jokers, so re-deal if one is drawn. do { if (gameState.aiDeck.length === 0) { gameState.aiDeck = CardSystem.createDeck(); } cardData = gameState.aiDeck.pop(); } while (cardData.suit === 'joker'); // After every 10 waves, new cards dealt start one level higher var startLevel = Math.floor((WaveSystem.waveNumber - 1) / 10) + 1; // Find best empty slot (prefer completing rows) var bestSlot = this.findBestEmptySlot(); if (bestSlot) { var self = this; var pos = getSlotPosition(bestSlot.row, bestSlot.col, false); // Set initial off-screen position and properties for animation var startY = -SLOT_HEIGHT; // Come from top of the screen var aiCardBack = LK.getAsset('aiCardBack', { anchorX: 0.5, anchorY: 0.5 }); aiCardBack.x = pos.x; aiCardBack.y = startY; aiCardBack.rotation = Math.PI * 4; // Two full spins aiCardBack.scale.set(0.1); // Start small gameLayer.addChild(aiCardBack); // Deal animation: spin into place tween(aiCardBack, { y: pos.y, rotation: 0, scaleX: 1, scaleY: 1 }, { duration: 600, easing: tween.quadOut, onFinish: function onFinish() { // Flip animation: shrink back tween(aiCardBack, { scaleX: 0.01 }, { duration: 150, easing: tween.quadIn, onFinish: function onFinish() { if (aiCardBack.parent) { aiCardBack.parent.removeChild(aiCardBack); } // Create and show the real card var card = new Card(cardData); card.setLevel(startLevel); card.activate(pos.x, pos.y, true, false); card.scale.x = 0.01; gameLayer.addChild(card); gameState.aiPlayArea[bestSlot.row][bestSlot.col] = card; // Flip animation: expand card face tween(card, { scaleX: 1.0 }, { duration: 150, easing: tween.quadOut, onFinish: function onFinish() { LK.getSound('cardLand').play(); self.applyAIHandBonuses(); } }); } }); } }); } }, findBestEmptySlot: function findBestEmptySlot() { var emptySlots = []; for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { if (!gameState.aiPlayArea[row][col]) { var score = this.evaluateSlotScore(row, col); emptySlots.push({ row: row, col: col, score: score }); } } } if (emptySlots.length === 0) { return null; } // Sort by score (higher is better) emptySlots.sort(function (a, b) { return b.score - a.score; }); return emptySlots[0]; }, evaluateSlotScore: function evaluateSlotScore(row, col) { var score = 0; var cardsInRow = 0; // Count cards in this row for (var c = 0; c < PLAY_AREA_COLS; c++) { if (gameState.aiPlayArea[row][c]) { cardsInRow++; } } // Prefer completing rows score += cardsInRow * 10; // Slight preference for middle positions score += (2 - Math.abs(col - 2)) * 2; return score; }, applyAIHandBonuses: function applyAIHandBonuses() { // Apply poker hand bonuses to AI cards for (var row = 0; row < PLAY_AREA_ROWS; row++) { var handEval = evaluateRowHand(row, false); var contributingCards = handEval.contributingCards || []; for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = gameState.aiPlayArea[row][col]; if (card) { card.handBonus = handEval.multiplier; card.calculateStats(); card.redOutline.visible = handEval.strength > 1 && contributingCards.indexOf(card) !== -1; } } } }, setupMods: function setupMods() { var playerModCount = 0; for (var suit in ModSystem.equippedMods) { if (ModSystem.equippedMods[suit]) { playerModCount++; } } var allMods = Object.keys(ModSystem.modData); var availableModsBySuit = { hearts: [], diamonds: [], clubs: [], spades: [] }; allMods.forEach(function (modId) { var suit = ModSystem.modData[modId].suit; if (suit && availableModsBySuit[suit]) { availableModsBySuit[suit].push(modId); } }); ModSystem.aiEquippedMods = { hearts: null, diamonds: null, clubs: null, spades: null }; var aiModCount = 0; var suits = ['hearts', 'diamonds', 'clubs', 'spades']; for (var i = suits.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = suits[i]; suits[i] = suits[j]; suits[j] = temp; } for (var i = 0; i < suits.length; i++) { var suit = suits[i]; if (aiModCount < playerModCount && availableModsBySuit[suit].length > 0) { var randomModForSuit = availableModsBySuit[suit][Math.floor(Math.random() * availableModsBySuit[suit].length)]; ModSystem.aiEquippedMods[suit] = randomModForSuit; aiModCount++; } } } }; /**** * Input Handling ****/ game.down = function (x, y, obj) { // Only handle input during playing state if (currentGameState !== 'playing') { return; } // Check if clicking on a card in hand for (var i = 0; i < gameState.playerHand.length; i++) { var card = gameState.playerHand[i]; if (card && Math.abs(x - card.x) < DEAL_SLOT_WIDTH / 2 && Math.abs(y - card.y) < DEAL_SLOT_HEIGHT / 2) { selectedCard = card; isDragging = true; originalCardPosition = { area: 'hand', index: i }; uiLayer.addChild(selectedCard); return; } } // Check if clicking on a card in play area for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = gameState.playerPlayArea[row][col]; if (card && Math.abs(x - card.x) < SLOT_WIDTH / 2 && Math.abs(y - card.y) < SLOT_HEIGHT / 2) { selectedCard = card; isDragging = true; originalCardPosition = { area: 'player', row: row, col: col }; // Remove from current position gameState.playerPlayArea[row][col] = null; gameLayer.addChild(selectedCard); return; } } } }; game.move = function (x, y, obj) { // Only handle input during playing state if (currentGameState !== 'playing') { return; } if (isDragging && selectedCard) { selectedCard.x = x; selectedCard.y = y; // Highlight mergeable cards // Player's play area for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = gameState.playerPlayArea[row][col]; if (card) { card.greenOutline.visible = selectedCard.canMergeWith(card); } } } // Player's hand for (var i = 0; i < gameState.playerHand.length; i++) { var card = gameState.playerHand[i]; if (card && card !== selectedCard) { card.greenOutline.visible = selectedCard.canMergeWith(card); } } // Switch to discard mode when dragging discardAreaGraphic.visible = false; refundButtonGraphic.visible = true; // Highlight discard area on hover var isOverDiscard = discardAreaContainer && x >= discardAreaContainer.x - DEAL_SLOT_WIDTH / 2 && x <= discardAreaContainer.x + DEAL_SLOT_WIDTH / 2 && y >= discardAreaContainer.y - DEAL_SLOT_HEIGHT / 2 && y <= discardAreaContainer.y + DEAL_SLOT_HEIGHT / 2; var chipRefund = Math.floor(gameState.dealCost / 2); discardText.setText('+' + formatNumberWithSuffix(chipRefund)); if (isOverDiscard) { discardText.fill = 0xffd700; // Gold color refundButtonGraphic.alpha = 1.0; } else { refundButtonGraphic.alpha = 0.7; discardText.fill = 0x999999; } } else { // When not dragging, show as deal button discardAreaGraphic.visible = true; refundButtonGraphic.visible = false; updateUI(); } }; game.up = function (x, y, obj) { // Only handle input during playing state if (currentGameState !== 'playing') { return; } if (isDragging && selectedCard) { isDragging = false; discardAreaGraphic.visible = true; refundButtonGraphic.visible = false; // Clear all temporary green outlines for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { var card = gameState.playerPlayArea[row][col]; if (card) { card.greenOutline.visible = false; } } } for (var i = 0; i < gameState.playerHand.length; i++) { var card = gameState.playerHand[i]; if (card) { card.greenOutline.visible = false; } } // Reset to deal button mode updateUI(); var targetSlot = getSlotFromPosition(x, y); if (targetSlot) { // Handle dropping card on discard area if (targetSlot.area === 'discard') { var chipRefund = Math.floor(gameState.dealCost / 2); gameState.playerChips += chipRefund; // Remove from original position data if (originalCardPosition.area === 'hand') { gameState.playerHand[originalCardPosition.index] = null; } else if (originalCardPosition.area === 'player') { // This is already null from game.down, so this is just for safety. gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = null; } // Remove card graphic from scene and memory if (selectedCard.parent) { selectedCard.parent.removeChild(selectedCard); } createFloatingText('+' + formatNumberWithSuffix(chipRefund) + ' Chips', selectedCard.x, selectedCard.y, 0xffd700); selectedCard = null; originalCardPosition = null; updateUI(); // Recalculate bonuses if a card was removed from play area if (originalCardPosition && originalCardPosition.area === 'player') { applyHandBonuses(); } return; // Exit early } if (targetSlot.area === 'player') { var existingCard = gameState.playerPlayArea[targetSlot.row][targetSlot.col]; if (existingCard && selectedCard.canMergeWith(existingCard)) { // Merge in play area var oldLevel = existingCard.level; var mergedCard = selectedCard.mergeWith(existingCard); if (mergedCard) { gameLayer.removeChild(existingCard); var handIndex = gameState.playerHand.indexOf(selectedCard); if (handIndex !== -1) { uiLayer.removeChild(selectedCard); gameState.playerHand[handIndex] = null; } else { gameLayer.removeChild(selectedCard); } var pos = getSlotPosition(targetSlot.row, targetSlot.col, true); mergedCard.activate(pos.x, pos.y, true); gameLayer.addChild(mergedCard); gameState.playerPlayArea[targetSlot.row][targetSlot.col] = mergedCard; mergedCard.alpha = 0; mergedCard.scaleX = mergedCard.scaleY = 2; tween(mergedCard, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.elasticOut }); LK.getSound('levelUp').play(); createFloatingText('+Level Up!', mergedCard.x, mergedCard.y - 50, 0x00ff00, 60); var damageBefore = Math.floor(10 * Math.pow(1.6, oldLevel - 1)); var fireRateBefore = Math.max(15, Math.floor(60 / Math.pow(1.2, oldLevel - 1))); var damageAfter = Math.floor(10 * Math.pow(1.6, mergedCard.level - 1)); var fireRateAfter = Math.max(15, Math.floor(60 / Math.pow(1.2, mergedCard.level - 1))); var damageIncrease = damageBefore > 0 ? Math.round((damageAfter / damageBefore - 1) * 100) : 0; var fireRateIncrease = fireRateAfter > 0 && fireRateAfter < fireRateBefore ? Math.round((fireRateBefore / fireRateAfter - 1) * 100) : 0; var statText = ""; if (damageBefore > 0) { var dmgMult = (damageAfter / damageBefore).toFixed(2); statText = "Dmg x" + dmgMult; } createFloatingText(statText, mergedCard.x, mergedCard.y + 55, 0x00ff00, 35); } } else if (!existingCard) { // Prevent Jokers from being played on the board directly. They can only merge. if (selectedCard.cardData.suit === 'joker') { createFloatingText('Jokers can only level up other cards.', selectedCard.x, selectedCard.y, 0xffcc00); returnCardToOriginalPosition(); } else { // Place in empty slot var pos = getSlotPosition(targetSlot.row, targetSlot.col, true); selectedCard.activate(pos.x, pos.y, true); selectedCard.resetInvestment(); var handIndex = gameState.playerHand.indexOf(selectedCard); if (handIndex !== -1) { uiLayer.removeChild(selectedCard); gameLayer.addChild(selectedCard); gameState.playerHand[handIndex] = null; } gameState.playerPlayArea[targetSlot.row][targetSlot.col] = selectedCard; } } else { // Prevent Jokers from being swapped onto the board. if (selectedCard.cardData.suit === 'joker') { createFloatingText('Jokers can only level up other cards.', selectedCard.x, selectedCard.y, 0xffcc00); returnCardToOriginalPosition(); } else { // Card exists, but cannot merge: swap them with animation var swappedCard = gameState.playerPlayArea[targetSlot.row][targetSlot.col]; // Determine target positions for animation var pos1 = getSlotPosition(targetSlot.row, targetSlot.col, true); var pos2; tween(selectedCard, { x: pos1.x, y: pos1.y }, { duration: 200, easing: tween.quadOut }); // Update selectedCard state selectedCard.isInPlay = true; if (originalCardPosition.area === 'hand') { selectedCard.resetInvestment(); // Card moved from hand to play area gameState.playerHand[originalCardPosition.index] = null; uiLayer.removeChild(selectedCard); gameLayer.addChild(selectedCard); } gameState.playerPlayArea[targetSlot.row][targetSlot.col] = selectedCard; // Determine target for swappedCard and animate if (originalCardPosition.area === 'player') { pos2 = getSlotPosition(originalCardPosition.row, originalCardPosition.col, true); swappedCard.isInPlay = true; gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = swappedCard; } else { // original was hand, so swapped card goes to hand var origHandIndex = originalCardPosition.index; var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; var handStartX = (SCREEN_WIDTH - handWidth) / 2; pos2 = { x: handStartX + origHandIndex * DEAL_SLOT_WIDTH + origHandIndex * 30 + DEAL_SLOT_WIDTH / 2, y: PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2 }; swappedCard.isInPlay = false; gameLayer.removeChild(swappedCard); uiLayer.addChild(swappedCard); gameState.playerHand[origHandIndex] = swappedCard; } tween(swappedCard, { x: pos2.x, y: pos2.y }, { duration: 200, easing: tween.quadOut }); selectedCard.calculateStats(); swappedCard.calculateStats(); } } } else if (targetSlot.area === 'hand') { var existingCard = gameState.playerHand[targetSlot.index]; if (existingCard && existingCard !== selectedCard && selectedCard.canMergeWith(existingCard)) { // Merge in hand var oldLevel = existingCard.level; var mergedCard = selectedCard.mergeWith(existingCard); if (mergedCard) { // Remove old cards var handIndex1 = gameState.playerHand.indexOf(selectedCard); if (handIndex1 !== -1) { uiLayer.removeChild(selectedCard); gameState.playerHand[handIndex1] = null; } else { gameLayer.removeChild(selectedCard); } var handIndex2 = gameState.playerHand.indexOf(existingCard); if (handIndex2 !== -1) { uiLayer.removeChild(existingCard); gameState.playerHand[handIndex2] = null; } // Place merged card in hand var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px var handStartX = (SCREEN_WIDTH - handWidth) / 2; var slotX = handStartX + targetSlot.index * DEAL_SLOT_WIDTH + targetSlot.index * 30 + DEAL_SLOT_WIDTH / 2; var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2; mergedCard.activate(slotX, slotY, false, true); uiLayer.addChild(mergedCard); gameState.playerHand[targetSlot.index] = mergedCard; // Merge animation mergedCard.alpha = 0; mergedCard.scaleX = mergedCard.scaleY = 2; tween(mergedCard, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.elasticOut }); LK.getSound('levelUp').play(); createFloatingText('+Level Up!', mergedCard.x, mergedCard.y - 50, 0x00ff00, 60); var damageBefore = Math.floor(10 * Math.pow(1.6, oldLevel - 1)); var fireRateBefore = Math.max(15, Math.floor(60 / Math.pow(1.2, oldLevel - 1))); var damageAfter = Math.floor(10 * Math.pow(1.6, mergedCard.level - 1)); var fireRateAfter = Math.max(15, Math.floor(60 / Math.pow(1.2, mergedCard.level - 1))); var damageIncrease = damageBefore > 0 ? Math.round((damageAfter / damageBefore - 1) * 100) : 0; var fireRateIncrease = fireRateAfter > 0 && fireRateAfter < fireRateBefore ? Math.round((fireRateBefore / fireRateAfter - 1) * 100) : 0; var statText = ""; if (damageBefore > 0) { var dmgMult = (damageAfter / damageBefore).toFixed(2); statText = "Dmg x" + dmgMult; } createFloatingText(statText, mergedCard.x, mergedCard.y + 55, 0x00ff00, 35); } else { returnCardToOriginalPosition(); } } else if (existingCard && existingCard !== selectedCard) { // Cannot merge, so swap with animation var swappedCard = gameState.playerHand[targetSlot.index]; // Get target positions var targetHandIndex = targetSlot.index; var handWidth1 = 5 * DEAL_SLOT_WIDTH + 4 * 30; var handStartX1 = (SCREEN_WIDTH - handWidth1) / 2; var pos1 = { x: handStartX1 + targetHandIndex * DEAL_SLOT_WIDTH + targetHandIndex * 30 + DEAL_SLOT_WIDTH / 2, y: PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2 }; var pos2; // Animate selectedCard tween(selectedCard, { x: pos1.x, y: pos1.y }, { duration: 200, easing: tween.quadOut }); // Update selectedCard state selectedCard.isInPlay = false; // It's going to a hand slot if (originalCardPosition.area === 'player') { selectedCard.resetInvestment(); // Card moved from play area to hand - should not happen here, but for safety. Investment stops due to isInPlay=false gameLayer.removeChild(selectedCard); uiLayer.addChild(selectedCard); } else { // This is a hand-to-hand swap, timer should not be reset. gameState.playerHand[originalCardPosition.index] = null; } gameState.playerHand[targetHandIndex] = selectedCard; // Update swappedCard state if (originalCardPosition.area === 'player') { pos2 = getSlotPosition(originalCardPosition.row, originalCardPosition.col, true); swappedCard.isInPlay = true; uiLayer.removeChild(swappedCard); gameLayer.addChild(swappedCard); gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = swappedCard; } else { // original was hand var origHandIndex = originalCardPosition.index; var handWidth2 = 5 * DEAL_SLOT_WIDTH + 4 * 30; var handStartX2 = (SCREEN_WIDTH - handWidth2) / 2; pos2 = { x: handStartX2 + origHandIndex * DEAL_SLOT_WIDTH + origHandIndex * 30 + DEAL_SLOT_WIDTH / 2, y: PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2 }; swappedCard.isInPlay = false; gameState.playerHand[origHandIndex] = swappedCard; } tween(swappedCard, { x: pos2.x, y: pos2.y }, { duration: 200, easing: tween.quadOut }); selectedCard.calculateStats(); swappedCard.calculateStats(); } else { returnCardToOriginalPosition(); } } } else { returnCardToOriginalPosition(); } selectedCard = null; originalCardPosition = null; applyHandBonuses(); } }; function returnCardToOriginalPosition() { if (!selectedCard || !originalCardPosition) { return; } if (originalCardPosition.area === 'hand') { // Return to hand slot var handIndex = originalCardPosition.index; var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px var handStartX = (SCREEN_WIDTH - handWidth) / 2; var slotX = handStartX + handIndex * DEAL_SLOT_WIDTH + handIndex * 30 + DEAL_SLOT_WIDTH / 2; var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2; selectedCard.x = slotX; selectedCard.y = slotY; gameState.playerHand[handIndex] = selectedCard; } else if (originalCardPosition.area === 'player') { // Return to play area slot var pos = getSlotPosition(originalCardPosition.row, originalCardPosition.col, true); selectedCard.x = pos.x; selectedCard.y = pos.y; gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = selectedCard; } } function createOutlinedText(text, options, outlineColor, outlineThickness) { var container = new Container(); // Create the outline by placing shadow texts in 8 directions var offsets = [[-outlineThickness, -outlineThickness], // top-left [0, -outlineThickness], // top [outlineThickness, -outlineThickness], // top-right [outlineThickness, 0], // right [outlineThickness, outlineThickness], // bottom-right [0, outlineThickness], // bottom [-outlineThickness, outlineThickness], // bottom-left [-outlineThickness, 0] // left ]; // Create outline shadows offsets.forEach(function (offset) { var shadowText = new Text2(text, { size: options.size, fill: outlineColor, weight: options.weight // Don't use stroke properties for shadows }); shadowText.anchor.set(options.anchor ? options.anchor[0] : 0.5, options.anchor ? options.anchor[1] : 0.5); shadowText.x = offset[0]; shadowText.y = offset[1]; container.addChild(shadowText); }); // Create the main text on top var mainText = new Text2(text, { size: options.size, fill: options.fill, weight: options.weight }); mainText.anchor.set(options.anchor ? options.anchor[0] : 0.5, options.anchor ? options.anchor[1] : 0.5); container.addChild(mainText); // Add a setText method to the container for easy updates container.setText = function (newText) { // Update all shadow texts for (var i = 0; i < offsets.length; i++) { container.children[i].setText(newText); } // Update main text mainText.setText(newText); }; return container; } function createFloatingText(text, x, y, color, size) { var textOptions = { size: size || 40, fill: color || 0xffffff, weight: 800, anchor: [0.5, 0.5] }; var outlineColor = 0x000000; var outlineThickness = 3; // Add black outline for green level up text if (color === 0x00ff00) { outlineThickness = 6; textOptions.size = (size || 40) * 1.5; // Make it smaller } // Make poker damage text white with black outline, bigger, and animate faster if (color === 0xff0000 && text.startsWith('-')) { textOptions.fill = 0xffffff; // White color outlineColor = 0x000000; // Black outline outlineThickness = 6; // Black outline thickness textOptions.size = (size || 40) * 1.5; // Make it 50% bigger (increased from 30%) } var floatingText = createOutlinedText(text, textOptions, outlineColor, outlineThickness); floatingText.x = x; floatingText.y = y; floatingText.alpha = 1; uiLayer.addChild(floatingText); // Use larger animation for important messages var animationDistance = size && size > 40 ? 120 : 80; var animationDuration = size && size > 40 ? 2000 : 1500; // Make poker damage text animate faster if (color === 0xff0000 && text.startsWith('-')) { animationDuration = 800; // Much faster animation } // Level up text fades faster now if (color === 0x00ff00) { animationDuration = 1000; } // Split animation into two parts var floatDuration = animationDuration * 0.6; // 60% of time for floating up var fadeDuration = animationDuration * 0.4; // 40% of time for fading out // First part: Float upwards with no alpha change tween(floatingText, { y: y - animationDistance * 0.7 // Float up 70% of the distance }, { duration: floatDuration, easing: tween.quadOut, onFinish: function onFinish() { // Second part: Continue floating while fading out tween(floatingText, { y: y - animationDistance, alpha: 0 }, { duration: fadeDuration, easing: tween.quadOut, onFinish: function onFinish() { uiLayer.removeChild(floatingText); } }); } }); } function createExplosion(x, y, color) { var numParticles = 5; for (var i = 0; i < numParticles; i++) { var particle = LK.getAsset('explosionParticle', { anchorX: 0.5, anchorY: 0.5 }); particle.x = x; particle.y = y; particle.tint = color; particle.scale.set(0.5 + Math.random() * 0.5); var angle = Math.random() * Math.PI * 2; var distance = Math.random() * 50 + 20; var duration = 500 + Math.random() * 500; var targetX = x + Math.cos(angle) * distance; var targetY = y + Math.sin(angle) * distance; gameLayer.addChild(particle); tween(particle, { x: targetX, y: targetY, alpha: 0 }, { duration: duration, easing: tween.quadOut, onFinish: function (p) { return function () { if (p.parent) { p.parent.removeChild(p); } }; }(particle) }); } } /**** * Main Game Loop ****/ game.update = function () { // Update tutorial system if (TutorialSystem.isActive) { TutorialSystem.update(); } // Only run game logic when in playing state if (currentGameState !== 'playing') { if (currentGameState === 'start' && backgroundSuits.length > 0) { var speed = 0.5; var spacing = 400; // Calculate pattern dimensions var numCols = Math.ceil(SCREEN_WIDTH / spacing) + 3; var numRows = Math.ceil(SCREEN_HEIGHT / spacing) + 3; var patternWidth = numCols * spacing; var patternHeight = numRows * spacing; for (var i = 0; i < backgroundSuits.length; i++) { var suit = backgroundSuits[i]; // Move diagonally suit.x += speed; suit.y += speed; // Wrap horizontally if (suit.x > SCREEN_WIDTH + spacing * 1.5) { suit.x -= patternWidth; } // Wrap vertically if (suit.y > SCREEN_HEIGHT + spacing * 1.5) { suit.y -= patternHeight; } } } return; } if (!gameReady) { return; } // Update wave spawning system WaveSystem.update(); // Update active chips for (var i = activePlayerChips.length - 1; i >= 0; i--) { activePlayerChips[i].update(); } for (var i = activeAIChips.length - 1; i >= 0; i--) { activeAIChips[i].update(); } // Tutorial Assistance: If an enemy on the player's side gets halfway, destroy it. if (TutorialSystem.isActive) { var assistancePathThreshold = 50; // Halfway down the path for (var i = activePlayerChips.length - 1; i >= 0; i--) { var chip = activePlayerChips[i]; if (chip.pathProgress > assistancePathThreshold) { if (!TutorialSystem.assistanceMessageShown) { TutorialSystem.assistanceMessageShown = true; var message = "It looks like you could use some help! I've taken care of that enemy for you.\n\nThis helping hand is only available in the tutorial. Try adding more cards, merging them, and aiming for better poker hands to get stronger!"; TutorialSystem.showMessage(message, null, function () { TutorialSystem.clearMessage(); }); } // Instantly destroy the chip to assist the player, preventing them from losing in the tutorial. chip.takeDamage(chip.health * 2); } } } // Update active mines for (var i = activeMines.length - 1; i >= 0; i--) { if (activeMines[i]) { activeMines[i].update(); } } // Update active bullets for (var i = activeBullets.length - 1; i >= 0; i--) { activeBullets[i].update(); } // Update cards in play for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { if (gameState.playerPlayArea[row][col]) { gameState.playerPlayArea[row][col].update(); } if (gameState.aiPlayArea[row][col]) { gameState.aiPlayArea[row][col].update(); } } } // Update AI AISystem.update(); // Check win/lose conditions // Update life displays while (gameState.playerLives < lastPlayerLives) { lastPlayerLives--; var heartToFade = playerLifeHearts[lastPlayerLives]; if (heartToFade) { tween(heartToFade, { alpha: 0.2 }, { duration: 500 }); } } while (gameState.aiLives < lastAiLives) { lastAiLives--; var heartToFade = aiLifeHearts[lastAiLives]; if (heartToFade) { tween(heartToFade, { alpha: 0.2 }, { duration: 500 }); } } if (gameMode === 'coop') { if (gameState.playerLives <= 0) { showCoopResultsScreen(); } } else { if (gameState.playerLives <= 0) { showGameOver(false); } else if (gameState.aiLives <= 0) { showGameOver(true); } } updateUI(); }; /**** * Game Over ****/ function resetGameAndReturnToMenu() { // This function now encapsulates all the logic to reset the game to its initial state. // 1. Remove all dynamically created game objects from the stage. [].concat(_toConsumableArray(activePlayerChips), _toConsumableArray(activeAIChips), _toConsumableArray(activeBullets), _toConsumableArray(activeMines)).forEach(function (obj) { if (obj.parent) { obj.parent.removeChild(obj); } }); for (var r = 0; r < PLAY_AREA_ROWS; r++) { for (var c = 0; c < PLAY_AREA_COLS; c++) { var pCard = gameState.playerPlayArea[r][c]; if (pCard && pCard.parent) { pCard.parent.removeChild(pCard); } var aCard = gameState.aiPlayArea[r][c]; if (aCard && aCard.parent) { aCard.parent.removeChild(aCard); } } } gameState.playerHand.forEach(function (card) { if (card && card.parent) { card.parent.removeChild(card); } }); playerLifeHearts.forEach(function (h) { if (h.parent) { h.parent.removeChild(h); } }); aiLifeHearts.forEach(function (h) { if (h.parent) { h.parent.removeChild(h); } }); playerHandNameTexts.forEach(function (t) { if (t && t.parent) { t.parent.removeChild(t); } }); if (opponentNameText && opponentNameText.parent) { opponentNameText.parent.removeChild(opponentNameText); } if (playerNameText && playerNameText.parent) { playerNameText.parent.removeChild(playerNameText); } slotIndicators.forEach(function (indicator) { if (indicator.parent) { indicator.parent.removeChild(indicator); } }); slotIndicators = []; // 2. Clear all tracking arrays. activePlayerChips = []; activeAIChips = []; activeBullets = []; activeMines = []; playerLifeHearts = []; aiLifeHearts = []; playerHandNameTexts = []; opponentNameText = null; playerNameText = null; // 3. Reset game state variables. gameState.playerChips = 200; gameState.aiChips = 200; gameState.playerLives = 3; gameState.aiLives = 3; gameState.isPlayerTurn = true; gameState.dealCost = 25; // Reset to starting cost of 25 gameState.dealCount = 0; // Reset to starting count gameState.playerDeck = []; gameState.playerHand = []; gameState.playerPlayArea = []; gameState.aiDeck = []; gameState.aiPlayArea = []; // 4. Reset game systems. WaveSystem.waveNumber = 1; WaveSystem.waveTimer = 0; WaveSystem.bossSpawned = false; WaveSystem.playerBossDefeated = false; WaveSystem.aiBossDefeated = false; AISystem.thinkTimer = 0; // 5. Reset other global variables. lastPlayerLives = 0; lastAiLives = 0; selectedCard = null; isDragging = false; originalCardPosition = null; // 6. Hide persistent UI. gameElements.forEach(function (el) { el.visible = false; }); background.visible = false; coopBackground.visible = false; // 7. Return to the main menu. createStartScreen(true); } function showResultsScreen() { var resultsContainer = new Container(); uiLayer.addChild(resultsContainer); // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.7; resultsContainer.addChild(bg); var title = new Text2('VICTORY!', { size: 180, fill: 0xffd700, weight: 'bold', stroke: 0x000000, strokeThickness: 10 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 300; resultsContainer.addChild(title); // Calculate Battle Rating var oldRating = storage.battleRating; var waveBonus = (WaveSystem.waveNumber - 1) * 5; var livesBonus = (gameState.playerLives - gameState.aiLives) * 10; var winBonus = 25; var ratingChange = waveBonus + livesBonus + winBonus; ratingChange = Math.max(5, ratingChange); // Minimum 5 rating gain on win var newRating = oldRating + ratingChange; storage.battleRating = newRating; // Display Rating Calculation var yPos = 600; var yStep = 120; var ySubStep = 90; var oldRatingText = new Text2('Previous Rating: ' + oldRating, { size: 80, fill: 0xffffff, weight: 'bold' }); oldRatingText.anchor.set(0.5, 0.5); oldRatingText.x = SCREEN_WIDTH / 2; oldRatingText.y = yPos; resultsContainer.addChild(oldRatingText); yPos += yStep; var waveBonusText = new Text2('Wave Bonus (Wave ' + WaveSystem.waveNumber + '): +' + waveBonus, { size: 70, fill: 0x88ff88, weight: 'normal' }); waveBonusText.anchor.set(0.5, 0.5); waveBonusText.x = SCREEN_WIDTH / 2; waveBonusText.y = yPos; resultsContainer.addChild(waveBonusText); yPos += ySubStep; var livesBonusText; if (livesBonus >= 0) { livesBonusText = new Text2('Performance Bonus: +' + livesBonus, { size: 70, fill: 0x88ff88, weight: 'normal' }); } else { livesBonusText = new Text2('Performance Bonus: ' + livesBonus, { size: 70, fill: 0xff8888, weight: 'normal' }); } livesBonusText.anchor.set(0.5, 0.5); livesBonusText.x = SCREEN_WIDTH / 2; livesBonusText.y = yPos; resultsContainer.addChild(livesBonusText); yPos += ySubStep; var winBonusText = new Text2('Victory Bonus: +' + winBonus, { size: 70, fill: 0x88ff88, weight: 'normal' }); winBonusText.anchor.set(0.5, 0.5); winBonusText.x = SCREEN_WIDTH / 2; winBonusText.y = yPos; resultsContainer.addChild(winBonusText); yPos += yStep + 20; var lineBreak = LK.getAsset('lineBreak', { anchorX: 0.5, anchorY: 0.5 }); lineBreak.x = SCREEN_WIDTH / 2; lineBreak.y = yPos; lineBreak.scale.set(0.8); resultsContainer.addChild(lineBreak); yPos += yStep; var newRatingText = new Text2('New Rating: ' + newRating, { size: 100, fill: 0xffffff, weight: 'bold' }); newRatingText.anchor.set(0.5, 0.5); var battleRatingIcon = LK.getAsset('battleRatingIcon', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); var totalWidth = battleRatingIcon.width + 15 + newRatingText.width; battleRatingIcon.x = SCREEN_WIDTH / 2 - totalWidth / 2 + battleRatingIcon.width / 2; battleRatingIcon.y = yPos; newRatingText.x = battleRatingIcon.x + battleRatingIcon.width / 2 + 15 + newRatingText.width / 2; newRatingText.y = yPos; resultsContainer.addChild(battleRatingIcon); resultsContainer.addChild(newRatingText); // Create chest container var chestContainer = new Container(); chestContainer.x = SCREEN_WIDTH / 2; chestContainer.y = 1600; resultsContainer.addChild(chestContainer); // Add small chest var chest = LK.getAsset('smallChest', { anchorX: 0.5, anchorY: 0.5 }); chest.scale.set(0.01); chestContainer.addChild(chest); // Tap to open text var tapToOpenText = new Text2('Tap to open', { size: 60, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); tapToOpenText.anchor.set(0.5, 0.5); tapToOpenText.y = 120; tapToOpenText.visible = false; chestContainer.addChild(tapToOpenText); // Make chest interactive chestContainer.interactive = true; chestContainer.hitArea = new Rectangle(-150, -150, 300, 300); var chestOpened = false; var moneyReward = 0; // Animate chest in with bounce tween(chest, { scaleX: 1.5, scaleY: 1.5 }, { duration: 800, easing: tween.elasticOut, onFinish: function onFinish() { tapToOpenText.visible = true; // Pulse animation for tap to open text function pulseText() { if (!tapToOpenText.parent || chestOpened) { return; } tween(tapToOpenText, { scaleX: 1.1, scaleY: 1.1 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (!tapToOpenText.parent || chestOpened) { return; } tween(tapToOpenText, { scaleX: 1, scaleY: 1 }, { duration: 600, easing: tween.easeInOut, onFinish: pulseText }); } }); } pulseText(); } }); chestContainer.down = function () { if (chestOpened) { return; } chestOpened = true; tapToOpenText.visible = false; // Wiggle animation tween(chest, { rotation: -0.1 }, { duration: 100, easing: tween.linear, onFinish: function onFinish() { tween(chest, { rotation: 0.1 }, { duration: 100, easing: tween.linear, onFinish: function onFinish() { tween(chest, { rotation: 0 }, { duration: 100, easing: tween.linear, onFinish: function onFinish() { // Flash screen white LK.effects.flashScreen(0xffffff, 300); // Replace with open chest chestContainer.removeChild(chest); var openChest = LK.getAsset('smallChestOpen', { anchorX: 0.5, anchorY: 0.5 }); openChest.scale.set(1.5); chestContainer.addChild(openChest); // Generate random money reward moneyReward = Math.floor(Math.random() * 151) + 100; // 100-250 var startMoney = storage.money; var targetMoney = startMoney + moneyReward; // Money text animation var moneyText = new Text2('$' + moneyReward, { size: 150, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 8 }); moneyText.anchor.set(0.5, 0.5); moneyText.y = -50; moneyText.scale.set(0.1); moneyText.alpha = 0; chestContainer.addChild(moneyText); // Animate scale property directly to avoid potential proxy issues with Text2 objects tween(moneyText.scale, { x: 1, y: 1 }, { duration: 500, easing: tween.backOut }); // Animate other properties on the text object itself tween(moneyText, { alpha: 1, y: -100 }, { duration: 500, easing: tween.backOut, onFinish: function onFinish() { // Rolling number animation var rollDuration = 1000; var startTime = Date.now(); var rollInterval = LK.setInterval(function () { var elapsed = Date.now() - startTime; var progress = Math.min(elapsed / rollDuration, 1); var easedProgress = 1 - Math.pow(1 - progress, 3); // Cubic ease out var currentValue = Math.floor(startMoney + moneyReward * easedProgress); storage.money = currentValue; if (progress >= 1) { LK.clearInterval(rollInterval); storage.money = targetMoney; // Show tap to continue var tapToContinueText = new Text2('Tap to continue', { size: 70, fill: 0x000000, weight: 'bold' }); tapToContinueText.anchor.set(0.5, 0.5); tapToContinueText.y = 200; chestContainer.addChild(tapToContinueText); // Update chest interaction chestContainer.down = function () { tween(resultsContainer, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (resultsContainer.parent) { resultsContainer.parent.removeChild(resultsContainer); } resetGameAndReturnToMenu(); } }); }; } }, 16); // ~60fps } }); } }); } }); } }); }; // Animate the screen in resultsContainer.alpha = 0; tween(resultsContainer, { alpha: 1 }, { duration: 500 }); } function showGameOver(playerWon) { // Stop all game action by changing the state. The main update loop will halt. LK.stopMusic(); currentGameState = 'gameover_transition'; // NEW: Immediately stop all active game objects and create particle effects stopAllActionWithParticles(); // Animate the victory or defeat logo. var logoAssetId = playerWon ? 'victoryLogo' : 'defeatLogo'; var endLogo = LK.getAsset(logoAssetId, { anchorX: 0.5, anchorY: 0.5, x: SCREEN_WIDTH / 2, y: SCREEN_HEIGHT / 2 }); endLogo.scale.set(0.01); uiLayer.addChild(endLogo); tween(endLogo, { scaleX: 1, scaleY: 1 }, { duration: 1200, easing: tween.elasticOut, onFinish: function onFinish() { // After the animation, wait a moment, then transition. LK.setTimeout(function () { if (endLogo.parent) { endLogo.parent.removeChild(endLogo); } if (playerWon) { if (TutorialSystem.isActive) { // In tutorial, we skip the normal results and go straight to reset/menu to continue tutorial resetGameAndReturnToMenu(); } else { showResultsScreen(); } } else { if (TutorialSystem.isActive) { // Player lost tutorial, let them retry from beginning storage.tutorialCompleted = false; } resetGameAndReturnToMenu(); } }, 1500); } }); } function showCoopResultsScreen() { LK.stopMusic(); currentGameState = 'gameover_transition'; // NEW: Immediately stop all active game objects and create particle effects stopAllActionWithParticles(); var resultsContainer = new Container(); uiLayer.addChild(resultsContainer); // ... rest of the function remains the same // Dark overlay var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.7; resultsContainer.addChild(bg); var title = new Text2('RESULTS', { size: 180, fill: 0xffd700, weight: 'bold', stroke: 0x000000, strokeThickness: 10 }); title.anchor.set(0.5, 0.5); title.x = SCREEN_WIDTH / 2; title.y = 300; resultsContainer.addChild(title); // Wave Reached var waveText = new Text2('Wave Reached: ' + WaveSystem.waveNumber, { size: 80, fill: 0xffffff, weight: 'bold' }); waveText.anchor.set(0.5, 0.5); waveText.x = SCREEN_WIDTH / 2; waveText.y = 600; resultsContainer.addChild(waveText); if (WaveSystem.waveNumber > (storage.coopHighestWave || 0)) { storage.coopHighestWave = WaveSystem.waveNumber; } // Damage percentages var totalDamage = playerTotalDamage + aiTotalDamage; var playerPercent = totalDamage > 0 ? Math.round(playerTotalDamage / totalDamage * 100) : 0; var aiPercent = totalDamage > 0 ? 100 - playerPercent : 0; var playerDamageText = new Text2('Player Damage: ' + playerPercent + '%', { size: 70, fill: 0x88ff88, weight: 'normal' }); playerDamageText.anchor.set(0.5, 0.5); playerDamageText.x = SCREEN_WIDTH / 2; playerDamageText.y = 750; resultsContainer.addChild(playerDamageText); var aiDamageText = new Text2('AI Damage: ' + aiPercent + '%', { size: 70, fill: 0xff8888, weight: 'normal' }); aiDamageText.anchor.set(0.5, 0.5); aiDamageText.x = SCREEN_WIDTH / 2; aiDamageText.y = 850; resultsContainer.addChild(aiDamageText); // Create chest container var chestContainer = new Container(); chestContainer.x = SCREEN_WIDTH / 2; chestContainer.y = 1600; resultsContainer.addChild(chestContainer); // Add small chest var chest = LK.getAsset('smallChest', { anchorX: 0.5, anchorY: 0.5 }); chest.scale.set(0.01); chestContainer.addChild(chest); // Tap to open text var tapToOpenText = new Text2('Tap to open', { size: 60, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 4 }); tapToOpenText.anchor.set(0.5, 0.5); tapToOpenText.y = 120; tapToOpenText.visible = false; chestContainer.addChild(tapToOpenText); // Make chest interactive chestContainer.interactive = true; chestContainer.hitArea = new Rectangle(-150, -150, 300, 300); var chestOpened = false; // Animate chest in with bounce tween(chest, { scaleX: 1.5, scaleY: 1.5 }, { duration: 800, easing: tween.elasticOut, onFinish: function onFinish() { tapToOpenText.visible = true; // Pulse animation for tap to open text function pulseText() { if (!tapToOpenText.parent || chestOpened) { return; } tween(tapToOpenText, { scaleX: 1.1, scaleY: 1.1 }, { duration: 600, easing: tween.easeInOut, onFinish: function onFinish() { if (!tapToOpenText.parent || chestOpened) { return; } tween(tapToOpenText, { scaleX: 1, scaleY: 1 }, { duration: 600, easing: tween.easeInOut, onFinish: pulseText }); } }); } pulseText(); } }); chestContainer.down = function () { if (chestOpened) { return; } chestOpened = true; tapToOpenText.visible = false; // Wiggle animation tween(chest, { rotation: -0.1 }, { duration: 100, easing: tween.linear, onFinish: function onFinish() { tween(chest, { rotation: 0.1 }, { duration: 100, easing: tween.linear, onFinish: function onFinish() { tween(chest, { rotation: 0 }, { duration: 100, easing: tween.linear, onFinish: function onFinish() { // Flash screen white LK.effects.flashScreen(0xffffff, 300); // Replace with open chest chestContainer.removeChild(chest); var openChest = LK.getAsset('smallChestOpen', { anchorX: 0.5, anchorY: 0.5 }); openChest.scale.set(1.5); chestContainer.addChild(openChest); // Reward calculation var moneyReward = Math.floor(WaveSystem.waveNumber * 5 + playerPercent); var startMoney = storage.money; var targetMoney = startMoney + moneyReward; // Money text animation var moneyText = new Text2('$' + moneyReward, { size: 150, fill: 0xffd700, weight: 'bold', stroke: 0x000000, strokeThickness: 8 }); moneyText.anchor.set(0.5, 0.5); moneyText.y = -50; moneyText.scale.set(0.1); moneyText.alpha = 0; chestContainer.addChild(moneyText); // Animate scale property directly to avoid potential proxy issues with Text2 objects tween(moneyText.scale, { x: 1, y: 1 }, { duration: 500, easing: tween.backOut }); // Animate other properties on the text object itself tween(moneyText, { alpha: 1, y: -100 }, { duration: 500, easing: tween.backOut, onFinish: function onFinish() { // Rolling number animation var rollDuration = 1000; var startTime = Date.now(); var rollInterval = LK.setInterval(function () { var elapsed = Date.now() - startTime; var progress = Math.min(elapsed / rollDuration, 1); var easedProgress = 1 - Math.pow(1 - progress, 3); // Cubic ease out var currentValue = Math.floor(startMoney + moneyReward * easedProgress); storage.money = currentValue; if (progress >= 1) { LK.clearInterval(rollInterval); storage.money = targetMoney; // Show tap to continue var tapToContinueText = new Text2('Tap to continue', { size: 70, fill: 0x000000, weight: 'bold' }); tapToContinueText.anchor.set(0.5, 0.5); tapToContinueText.y = 200; chestContainer.addChild(tapToContinueText); // Update chest interaction chestContainer.down = function () { tween(resultsContainer, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (resultsContainer.parent) { resultsContainer.parent.removeChild(resultsContainer); } resetGameAndReturnToMenu(); } }); }; } }, 16); // ~60fps } }); } }); } }); } }); }; // Animate the screen in resultsContainer.alpha = 0; tween(resultsContainer, { alpha: 1 }, { duration: 500 }); } // NEW: Function to stop all action and create particle effects function stopAllActionWithParticles() { // Create explosion particles for all remaining chips [].concat(activePlayerChips, activeAIChips).forEach(function (chip) { if (chip.active && chip.visible) { createChipExplosionParticles(chip.x, chip.y, chip.value); } chip.active = false; chip.visible = false; if (chip.parent) { chip.parent.removeChild(chip); } }); // Create explosion particles for bullets activeBullets.forEach(function (bullet) { if (bullet.active && bullet.visible) { var explosionColor = 0x333333; // Dark Grey for clubs/spades if (bullet.suit === 'hearts' || bullet.suit === 'diamonds') { explosionColor = 0xff0000; // Red for hearts/diamonds } createExplosion(bullet.x, bullet.y, explosionColor); } bullet.active = false; bullet.visible = false; if (bullet.parent) { bullet.parent.removeChild(bullet); } }); // Explode mines activeMines.forEach(function (mine) { if (mine.active && mine.visible) { createExplosion(mine.x, mine.y, 0xffa500); } mine.active = false; mine.visible = false; if (mine.parent) { mine.parent.removeChild(mine); } }); // Clear the arrays activePlayerChips.length = 0; activeAIChips.length = 0; activeBullets.length = 0; activeMines.length = 0; // Stop all card firing by marking them as not in play temporarily for (var row = 0; row < PLAY_AREA_ROWS; row++) { for (var col = 0; col < PLAY_AREA_COLS; col++) { if (gameState.playerPlayArea[row][col]) { gameState.playerPlayArea[row][col].isInPlay = false; } if (gameState.aiPlayArea[row][col]) { gameState.aiPlayArea[row][col].isInPlay = false; } } } } // NEW: Enhanced particle effect for chip explosions - celebratory version function createChipExplosionParticles(x, y, chipValue) { var numParticles = Math.min(20, 10 + chipValue); // More particles for celebration var chipColor = getChipColor(chipValue); for (var i = 0; i < numParticles; i++) { var particle = LK.getAsset('explosionParticle', { anchorX: 0.5, anchorY: 0.5 }); particle.x = x + (Math.random() - 0.5) * 40; // Start in a tight cluster particle.y = y + (Math.random() - 0.5) * 40; particle.tint = chipColor; particle.scale.set(0.4 + Math.random() * 0.8); // Launch particles upward in a celebratory fountain pattern var angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 0.8; // Mostly upward, some spread var initialSpeed = 300 + Math.random() * 400; // Strong upward velocity var gravity = 600; // Gravity pulls them down var velocityX = Math.cos(angle) * initialSpeed; var velocityY = Math.sin(angle) * initialSpeed; // Negative Y is upward var duration = 2000 + Math.random() * 1000; // 2-3 seconds of flight time var targetX = x + velocityX * (duration / 1000); var targetY = y + velocityY * (duration / 1000) + 0.5 * gravity * Math.pow(duration / 1000, 2); // Ensure particles fall well off screen for full celebration effect if (targetY < SCREEN_HEIGHT + 200) { targetY = SCREEN_HEIGHT + 200 + Math.random() * 300; } gameLayer.addChild(particle); // Animate with celebratory spinning and fading tween(particle, { x: targetX, y: targetY, alpha: 0, rotation: Math.random() * Math.PI * 6 // Extra spinning for celebration }, { duration: duration, easing: tween.quadIn, // Natural gravity acceleration onFinish: function (p) { return function () { if (p.parent) { p.parent.removeChild(p); } }; }(particle) }); } } // NEW: Helper function to get chip color based on value function getChipColor(chipValue) { switch (chipValue) { case 1: return 0xffff00; // Yellow case 5: return 0xff0000; // Red case 10: return 0x00ff00; // Green case 25: return 0x0000ff; // Blue case 100: return 0x8000ff; // Purple default: return 0xffffff; // White for boss chips } } /**** * Utility Functions ****/ function formatNumberWithSuffix(number) { var num = Math.round(number); if (num < 1000) { return num.toString(); } if (num < 1000000) { return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; } if (num < 1000000000) { return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; } if (num < 1000000000000) { return (num / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B'; } return (num / 1000000000000).toFixed(1).replace(/\.0$/, '') + 'T'; } function clearPlayerStorage() { storage.battleRating = 0; storage.money = 0; storage.equippedMods = null; storage.ownedMods = null; storage.randomModPrice = 300; storage.tutorialCompleted = false; // Also reset tutorial progress storage.coopHighestWave = 0; // Clear highest wave in coop // Also reset in-memory ModSystem if it's already initialized ModSystem.equippedMods = { hearts: null, diamonds: null, clubs: null, spades: null }; ModSystem.ownedMods = {}; createFloatingText("Player Storage Cleared!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0xff0000, 100); } var TutorialSystem = { isActive: false, step: 0, overlay: null, messageBox: null, pulsingElement: null, isWaitingForTap: false, isWaitingForDrag: false, isWaitingForMerge: false, isWaitingForChipKill: false, isWaitingForJokerMerge: false, nextStepCallback: null, assistanceMessageShown: false, startScreenItems: {}, registerStartScreenElements: function registerStartScreenElements(items) { this.startScreenItems = items; }, start: function start() { this.isActive = true; this.step = 0; this.assistanceMessageShown = false; }, startInGameTutorial: function startInGameTutorial() { this.isActive = true; this.step = 1; this.assistanceMessageShown = false; dealInitialHand(); var self = this; LK.setTimeout(function () { self.advanceStep(); }, 1500); }, update: function update() { if (!this.isActive) { return; } if (this.isWaitingForDrag) { for (var r = 0; r < PLAY_AREA_ROWS; r++) { for (var c = 0; c < PLAY_AREA_COLS; c++) { if (gameState.playerPlayArea[r][c]) { this.isWaitingForDrag = false; this.advanceStep(); return; } } } } if (this.isWaitingForMerge) { var mergeCompleted = false; // Check play area for a leveled-up card for (var r = 0; r < PLAY_AREA_ROWS; r++) { for (var c = 0; c < PLAY_AREA_COLS; c++) { if (gameState.playerPlayArea[r][c] && gameState.playerPlayArea[r][c].level >= 2) { mergeCompleted = true; break; } } if (mergeCompleted) { break; } } // If not found in play area, check the hand if (!mergeCompleted) { for (var i = 0; i < gameState.playerHand.length; i++) { if (gameState.playerHand[i] && gameState.playerHand[i].level >= 2) { mergeCompleted = true; break; } } } if (mergeCompleted) { this.isWaitingForMerge = false; this.advanceStep(); return; } } if (this.isWaitingForJokerMerge) { var hasLevel3PlusCard = false; var level2PlusCardCount = 0; // Check play area for cards for (var r = 0; r < PLAY_AREA_ROWS; r++) { for (var c = 0; c < PLAY_AREA_COLS; c++) { var card = gameState.playerPlayArea[r][c]; if (card) { if (card.level >= 3) { hasLevel3PlusCard = true; } if (card.level >= 2) { level2PlusCardCount++; } } } } // Also check player's hand for (var i = 0; i < gameState.playerHand.length; i++) { var card = gameState.playerHand[i]; if (card) { if (card.level >= 3) { hasLevel3PlusCard = true; } if (card.level >= 2) { level2PlusCardCount++; } } } if (hasLevel3PlusCard || level2PlusCardCount >= 2) { this.isWaitingForJokerMerge = false; this.advanceStep(); return; } } if (this.isWaitingForChipKill) { if (activePlayerChips.length === 0) { this.isWaitingForChipKill = false; this.advanceStep(); } } }, advanceStep: function advanceStep() { this.clearMessage(); this.step++; switch (this.step) { case 2: this.explainDrag(); break; case 3: this.waitForDrag(); break; case 4: this.spawnFirstEnemy(); break; case 5: this.waitForKill(); break; case 6: this.explainMerge(); break; case 7: this.waitForMerge(); break; case 8: this.explainJoker(); break; case 9: this.instructJokerMerge(); break; case 10: this.startWaves(); break; case 11: this.explainPokerHands(); break; case 12: this.explainDiscard(); break; case 13: this.guaranteedWin(); break; case 14: this.guideToShop(); break; case 15: this.guideToRandomMod(); break; case 16: this.guideToModsScreen(); break; case 17: this.explainEquipMod(); break; case 18: this.completeTutorial(); break; } }, showMessage: function showMessage(text, elementToPulse, waitForTapCallback, isPostGame) { this.clearMessage(); this.overlay = new Container(); if (!isPostGame) { this.overlay.interactive = true; } this.overlay.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); var bg = LK.getAsset('card', { width: SCREEN_WIDTH, height: SCREEN_HEIGHT, anchorX: 0, anchorY: 0 }); bg.tint = 0x000000; bg.alpha = 0.7; this.overlay.addChild(bg); uiLayer.addChild(this.overlay); this.messageBox = new Text2(text, { size: 70, fill: 0xffffff, weight: 'bold', stroke: 0x000000, strokeThickness: 5, align: 'center', wordWrap: true, wordWrapWidth: SCREEN_WIDTH - 200 }); this.messageBox.anchor.set(0.5, 0.5); this.messageBox.x = SCREEN_WIDTH / 2; this.messageBox.y = isPostGame ? SCREEN_HEIGHT - 650 : SCREEN_HEIGHT / 2; this.overlay.addChild(this.messageBox); if (elementToPulse) { this.pulsingElement = elementToPulse; if (elementToPulse.parent) { this.pulsingElementOriginalParent = elementToPulse.parent; this.overlay.addChild(elementToPulse); } this.pulseAnimation(elementToPulse); } if (waitForTapCallback) { this.isWaitingForTap = true; this.nextStepCallback = waitForTapCallback; this.overlay.down = function () { if (TutorialSystem.isWaitingForTap) { TutorialSystem.isWaitingForTap = false; TutorialSystem.nextStepCallback(); } }; } }, clearMessage: function clearMessage() { if (this.pulsingElement) { tween.stop(this.pulsingElement); if (this.pulsingElement.scale) { tween.stop(this.pulsingElement.scale); } if (this.pulsingElement.originalScaleX) { this.pulsingElement.scale.set(this.pulsingElement.originalScaleX, this.pulsingElement.originalScaleY); } if (this.pulsingElementOriginalParent) { this.pulsingElementOriginalParent.addChild(this.pulsingElement); this.pulsingElementOriginalParent = null; } this.pulsingElement = null; } if (this.overlay) { if (this.overlay.parent) { this.overlay.parent.removeChild(this.overlay); } this.overlay = null; this.messageBox = null; } }, pulseAnimation: function pulseAnimation(element) { var self = this; var originalScaleX = element.originalScaleX || (element.scale ? element.scale.x : 1); var originalScaleY = element.originalScaleY || (element.scale ? element.scale.y : 1); element.originalScaleX = originalScaleX; element.originalScaleY = originalScaleY; tween(element.scale, { x: originalScaleX * 1.1, y: originalScaleY * 1.1 }, { duration: 700, easing: tween.easeInOut, onFinish: function onFinish() { if (element.parent) { tween(element.scale, { x: originalScaleX, y: originalScaleY }, { duration: 700, easing: tween.easeInOut, onFinish: function onFinish() { if (element.parent) { self.pulseAnimation(element); } } }); } } }); }, explainDrag: function explainDrag() { var self = this; var cardInHand = gameState.playerHand.find(function (c) { return c !== null; }); if (cardInHand) { self.showMessage("You've been dealt 5 cards. Drag one to an empty slot on your board to play it.", cardInHand, self.advanceStep.bind(self)); } }, waitForDrag: function waitForDrag() { this.isWaitingForDrag = true; }, explainDiscard: function explainDiscard() { this.showMessage("You can also drag cards to the draw button to sell them for a refund.\n\nTap to continue.", discardAreaContainer, this.advanceStep.bind(this)); }, spawnFirstEnemy: function spawnFirstEnemy() { this.showMessage("Excellent! A card on the board will automatically attack enemies. Let's see it in action!", null, function () { TutorialSystem.clearMessage(); ChipSpawner.spawnChip(1, true); TutorialSystem.advanceStep(); }); }, waitForKill: function waitForKill() { this.isWaitingForChipKill = true; }, explainMerge: function explainMerge() { this.showMessage("Well done! You can level up cards by merging them. Drag a card onto another of the SAME SUIT or SAME VALUE to merge them.\nNote: When you merge, the new card will be a random value and suit, but its level will increase!", null, function () { TutorialSystem.clearMessage(); var cardIndices = []; for (var i = 0; i < gameState.playerHand.length; i++) { if (gameState.playerHand[i]) { cardIndices.push(i); } } if (cardIndices.length >= 2) { var index1 = cardIndices[0]; var index2 = cardIndices[1]; var oldCard1 = gameState.playerHand[index1]; var oldCard2 = gameState.playerHand[index2]; // Make them same suit to be mergeable var newCardData1 = { suit: 'spades', value: 'A', id: 'spades_A' }; var newCardData2 = { suit: 'spades', value: 'K', id: 'spades_K' }; if (oldCard1 && oldCard1.parent) { oldCard1.parent.removeChild(oldCard1); } if (oldCard2 && oldCard2.parent) { oldCard2.parent.removeChild(oldCard2); } placeCardInHand(new Card(newCardData1), index1); placeCardInHand(new Card(newCardData2), index2); } TutorialSystem.advanceStep(); }); }, waitForMerge: function waitForMerge() { this.isWaitingForMerge = true; }, explainJoker: function explainJoker() { this.showMessage("Fantastic! Jokers are special. They can merge with ANY card to raise its level.", null, function () { TutorialSystem.clearMessage(); // Place a Joker in hand var jokerData = { suit: 'joker', value: 'black', id: 'joker_black' }; placeCardInHand(new Card(jokerData), 2); // Place it in the middle of the hand TutorialSystem.advanceStep(); }); }, instructJokerMerge: function instructJokerMerge() { var self = this; var jokerInHand = gameState.playerHand[2]; if (jokerInHand) { var message = "Now drag the Joker from your hand onto a card on your board to level it up!"; // We pulse the joker, as that's the one to drag. this.showMessage(message, jokerInHand, function () { self.clearMessage(); self.isWaitingForJokerMerge = true; }); } else { // Failsafe this.advanceStep(); } }, startWaves: function startWaves() { this.showMessage("Perfect! Merging is key to victory. Now, a real wave is coming. The AI will also start playing.", null, function () { TutorialSystem.clearMessage(); gameReady = true; AISystem.thinkDelay = 240; TutorialSystem.advanceStep(); }); }, explainPokerHands: function explainPokerHands() { LK.setTimeout(function () { TutorialSystem.showMessage("Placing cards in rows creates poker hands, like 'pairs' or 'straights', giving them a massive damage boost!", null, function () { TutorialSystem.clearMessage(); TutorialSystem.advanceStep(); }); }, 8000); }, guaranteedWin: function guaranteedWin() {}, guideToShop: function guideToShop() { var items = this.startScreenItems; var message = "You won! As a reward, you get a FREE mod. Go to the shop to claim it."; this.setupPostGameGuide(message, items.shop.button, [items.battle.button, items.mods.button]); var shopClickHandler = function shopClickHandler() { items.shop.down(); TutorialSystem.clearMessage(); [items.battle.button, items.mods.button, items.shop.button].forEach(function (b) { b.interactive = true; b.alpha = 1; }); TutorialSystem.advanceStep(); }; items.shop.button.down = shopClickHandler; items.shop.icon.down = shopClickHandler; }, guideToRandomMod: function guideToRandomMod() { var items = this.startScreenItems.shopItems; var message = "Tap the glowing item to get your free random mod!"; var originalPrice = storage.randomModPrice; storage.randomModPrice = 0; items.priceText.setText('$0'); this.showMessage(message, null); if (this.overlay) { // Hide the dark background and make overlay non-interactive to allow clicks on shop this.overlay.getChildAt(0).visible = false; this.overlay.interactive = false; } items.overlay.interactive = items.asset.interactive = true; var originalBuy = items.buyButtonDown; items.overlay.down = items.asset.down = function () { items.overlay.down = items.asset.down = function () {}; // Prevent multi-click originalBuy(); storage.randomModPrice = originalPrice; LK.setTimeout(function () { TutorialSystem.clearMessage(); TutorialSystem.advanceStep(); }, 3500); }; }, guideToModsScreen: function guideToModsScreen() { var items = this.startScreenItems; items.shopContainer.visible = false; var message = "Awesome! Now let's equip your new mod. Go to the mods screen."; this.setupPostGameGuide(message, items.mods.button, [items.battle.button, items.shop.button]); var modsClickHandler = function modsClickHandler() { items.mods.down(); TutorialSystem.clearMessage(); [items.battle.button, items.mods.button, items.shop.button].forEach(function (b) { b.interactive = true; b.alpha = 1; }); TutorialSystem.advanceStep(); }; items.mods.button.down = modsClickHandler; items.mods.icon.down = modsClickHandler; }, explainEquipMod: function explainEquipMod() { // First, manually refresh the visual state of all mods in the grid // because the grid was created before the player owned the new mod. var modsContainer = this.startScreenItems.modsContainer; // The gridContainer is the last major element added to the modsContainer. var gridContainer = modsContainer.children[modsContainer.children.length - 1]; if (gridContainer && gridContainer.children.length > 0) { var suitModAssets = ['burnHeartMod', 'chipsDiamondMod', 'spreadClubMod', 'freezeSpadeMod', 'unityHeartsMod', 'gamblerDiamondsMod', 'slowClubsMod', 'mineSpadesMod', 'investmentHeartsMod', 'boostDiamondsMod', 'ricochetClubsMod', 'deathSpadesMod']; var childrenPerMod = 4; // Assumes cell, shadow, mod asset, name text for (var modIndex = 0; modIndex < suitModAssets.length; modIndex++) { var assetId = suitModAssets[modIndex]; var isOwned = ModSystem.isModOwned(assetId); var cellIndex = modIndex * childrenPerMod; var modAssetIndex = cellIndex + 2; if (gridContainer.children[cellIndex] && gridContainer.children[modAssetIndex]) { var cell = gridContainer.children[cellIndex]; var modAsset = gridContainer.children[modAssetIndex]; if (isOwned) { modAsset.tint = 0xffffff; modAsset.alpha = 1.0; cell.tint = 0xffffff; } else { modAsset.tint = 0x555555; modAsset.alpha = 0.6; cell.tint = 0x888888; } } } } var message = "This screen shows all mods. Tap your new mod, then tap 'Equip' to use it in battle!"; this.showMessage(message, null, function () { TutorialSystem.clearMessage(); var originalEquip = ModSystem.equipMod; ModSystem.equipMod = function (modAssetId) { originalEquip.call(ModSystem, modAssetId); TutorialSystem.advanceStep(); ModSystem.equipMod = originalEquip; }; }); // Make the tutorial overlay transparent so the mods screen is fully visible. if (this.overlay) { this.overlay.getChildAt(0).visible = false; } }, completeTutorial: function completeTutorial() { this.showMessage("Tutorial complete! You're ready for battle. Good luck!", null, function () { TutorialSystem.clearMessage(); storage.tutorialCompleted = true; TutorialSystem.isActive = false; var items = TutorialSystem.startScreenItems; [items.battle, items.mods, items.shop].forEach(function (item) { if (item && item.button && item.icon) { item.button.interactive = true; item.button.alpha = 1.0; item.icon.interactive = true; item.icon.alpha = 1.0; } }); TutorialSystem.startScreenItems.modsContainer.visible = false; TutorialSystem.startScreenItems.battle.down(); }); }, setupPostGameGuide: function setupPostGameGuide(message, elementToPulse, elementsToDisable) { this.clearMessage(); this.showMessage(message, null, null, true); // isPostGame = true var items = this.startScreenItems; // Disable other buttons and their icons elementsToDisable.forEach(function (el) { el.interactive = false; el.alpha = 0.5; if (el === items.battle.button) { items.battle.icon.interactive = false; items.battle.icon.alpha = 0.5; } else if (el === items.mods.button) { items.mods.icon.interactive = false; items.mods.icon.alpha = 0.5; } else if (el === items.shop.button) { items.shop.icon.interactive = false; items.shop.icon.alpha = 0.5; } }); // Enable the target button and its icon elementToPulse.interactive = true; elementToPulse.alpha = 1.0; var iconToPulse = null; if (elementToPulse === items.shop.button) { iconToPulse = items.shop.icon; } else if (elementToPulse === items.mods.button) { iconToPulse = items.mods.icon; } if (iconToPulse) { iconToPulse.interactive = true; iconToPulse.alpha = 1.0; } // Bring the container with all buttons on top of the overlay if (this.overlay && elementToPulse.parent && elementToPulse.parent.parent) { elementToPulse.parent.parent.addChild(elementToPulse.parent); } this.pulsingElement = elementToPulse; this.pulseAnimation(this.pulsingElement); } }; function placeCardInHand(card, index) { var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; var handStartX = (SCREEN_WIDTH - handWidth) / 2; var slotX = handStartX + index * DEAL_SLOT_WIDTH + index * 30 + DEAL_SLOT_WIDTH / 2; var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2; card.activate(slotX, slotY, false, true); uiLayer.addChild(card); gameState.playerHand[index] = card; } function displayHandInfo() { // Show current poker hand evaluations for debugging for (var row = 0; row < PLAY_AREA_ROWS; row++) { var handEval = evaluateRowHand(row, true); console.log('Player Row ' + row + ':', handEval.type, 'Multiplier:', handEval.multiplier); } } var background = LK.getAsset('background', { anchorX: 0.5, anchorY: 0 }); background.x = SCREEN_WIDTH / 2; background.y = 50; background.visible = false; gameLayer.addChild(background); var coopBackground = LK.getAsset('coopBackground', { anchorX: 0.5, anchorY: 0 }); coopBackground.x = SCREEN_WIDTH / 2; coopBackground.y = 50; coopBackground.visible = false; gameLayer.addChild(coopBackground); // Place two chipracks side by side in the center at the top of the screen var chipRack1 = LK.getAsset('chipRack', { anchorX: 0.5, anchorY: 0 }); var rackWidth = chipRack1.width; var rackHeight = chipRack1.height; chipRack1.x = SCREEN_WIDTH / 2 - rackWidth / 2; chipRack1.y = 60 - rackHeight * 0.75 + 30; chipRack1.visible = false; gameLayer.addChild(chipRack1); gameElements.push(chipRack1); var chipRack2 = LK.getAsset('chipRack', { anchorX: 0.5, anchorY: 0 }); chipRack2.x = SCREEN_WIDTH / 2 + rackWidth / 2; chipRack2.y = 60 - rackHeight * 0.75 + 30; chipRack2.visible = false; gameLayer.addChild(chipRack2); gameElements.push(chipRack2); var border = LK.getAsset('border', { anchorX: 0.5, anchorY: 0.5 }); border.x = SCREEN_WIDTH / 2; border.y = SCREEN_HEIGHT / 2; border.visible = false; gameLayer.addChild(border); gameElements.push(border); var bottomBar = LK.getAsset('bottomBar', { anchorX: 0.5, anchorY: 1 }); bottomBar.x = SCREEN_WIDTH / 2; bottomBar.y = SCREEN_HEIGHT; bottomBar.visible = false; gameLayer.addChild(bottomBar); gameElements.push(bottomBar); // Show start screen instead of initializing game immediately createStartScreen(); // Add active chips container below the bottom bar gameLayer.addChildAt(activePlayerChipsContainer, gameLayer.getChildIndex(bottomBar)); gameLayer.addChildAt(activeAIChipsContainer, gameLayer.getChildIndex(bottomBar)); // Add chipStack asset above chips display var chipStack = LK.getAsset('chipStack', { anchorX: 0.5, anchorY: 1 }); chipStack.x = playerChipsText.x + playerChipsText.width / 2; chipStack.y = playerChipsText.y - 10; chipStack.visible = false; uiLayer.addChild(chipStack); gameElements.push(chipStack); ; ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
battleRating: 0,
money: 0,
ownedMods: {},
randomModPrice: 300,
tutorialCompleted: false,
coopHighestWave: 0
});
/****
* Classes
****/
/****
* Bullet Class
****/
var Bullet = Container.expand(function () {
var self = Container.call(this);
self.active = false;
self.target = null;
self.damage = 10;
self.speed = 11.5;
self.isPlayerCard = true;
self.isSeekingLastPosition = false;
self.targetLastX = 0;
self.targetLastY = 0;
self.level = 1;
self.ricochetCount = 0;
self.ricochetedFrom = [];
var currentGraphic = null;
// Add suitCircle backgrounds for each suit
var suitCircleGraphics = {
'hearts': self.attachAsset('suitCircle', {
anchorX: 0.5,
anchorY: 0.5
}),
'diamonds': self.attachAsset('suitCircle', {
anchorX: 0.5,
anchorY: 0.5
}),
'clubs': self.attachAsset('suitCircle', {
anchorX: 0.5,
anchorY: 0.5
}),
'spades': self.attachAsset('suitCircle', {
anchorX: 0.5,
anchorY: 0.5
})
};
var baseSuitGraphics = {
'hearts': self.attachAsset('heartSuit', {
anchorX: 0.5,
anchorY: 0.5
}),
'diamonds': self.attachAsset('diamondSuit', {
anchorX: 0.5,
anchorY: 0.5
}),
'clubs': self.attachAsset('clubSuit', {
anchorX: 0.5,
anchorY: 0.5
}),
'spades': self.attachAsset('spadeSuit', {
anchorX: 0.5,
anchorY: 0.5
})
};
var modGraphicsCache = {};
// Set scale and hide all suit graphics and suit circles initially.
for (var suit in baseSuitGraphics) {
var graphic = baseSuitGraphics[suit];
var circle = suitCircleGraphics[suit];
graphic.scale.set(0.3); // Small bullets
graphic.visible = false;
if (circle) {
circle.scale.set(0.45); // Slightly larger than suit icon
circle.visible = false;
// Ensure circle is rendered below the suit icon
self.setChildIndex(circle, Math.max(0, self.getChildIndex(graphic) - 1));
}
}
self.activate = function (startX, startY, target, damage, suit, isPlayerCard, level) {
self.active = true;
self.visible = true;
self.x = startX;
self.y = startY;
self.target = target;
self.damage = damage;
self.isPlayerCard = isPlayerCard;
self.level = level || 1;
self.suit = suit || 'hearts';
self.isSeekingLastPosition = false;
self.ricochetCount = 0;
self.ricochetedFrom = [];
self.hasRetargeted = false;
if (suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', isPlayerCard) === 'ricochetClubsMod') {
// 2 bounces, +1 every 2 levels. Max 5.
self.ricochetCount = Math.min(5, 2 + Math.floor((self.level - 1) / 2));
}
if (self.target) {
self.targetLastX = self.target.x;
self.targetLastY = self.target.y;
}
// Hide previous suitCircle if any
if (currentGraphic) {
currentGraphic.visible = false;
// Hide all suit circles
for (var suitKey in suitCircleGraphics) {
if (suitCircleGraphics[suitKey]) {
suitCircleGraphics[suitKey].visible = false;
}
}
}
suit = suit || 'hearts';
var equippedMod = ModSystem.getEquippedModAsset(suit, self.isPlayerCard);
if (equippedMod) {
if (!modGraphicsCache[equippedMod]) {
modGraphicsCache[equippedMod] = self.attachAsset(equippedMod, {
anchorX: 0.5,
anchorY: 0.5
});
modGraphicsCache[equippedMod].scale.set(0.3);
modGraphicsCache[equippedMod].visible = false;
}
currentGraphic = modGraphicsCache[equippedMod];
} else {
currentGraphic = baseSuitGraphics[suit];
}
currentGraphic.visible = true;
// Show and position the suitCircle under the current suit asset
if (suitCircleGraphics[suit]) {
suitCircleGraphics[suit].visible = true;
// Ensure suitCircle is rendered below the suit icon
if (self.getChildIndex(suitCircleGraphics[suit]) > self.getChildIndex(currentGraphic)) {
self.setChildIndex(suitCircleGraphics[suit], Math.max(0, self.getChildIndex(currentGraphic) - 1));
}
// Center the circle (should already be, but for safety)
suitCircleGraphics[suit].x = 0;
suitCircleGraphics[suit].y = 0;
}
};
self.findNewTarget = function () {
// Use the same targeting logic as cards, but from bullet's perspective
var targets;
if (gameMode === 'coop') {
targets = activePlayerChips.concat(activeAIChips);
} else {
targets = self.isPlayerCard ? activePlayerChips : activeAIChips;
}
var bestTarget = null;
var shortestDistanceSq = Infinity;
var turningPointY = SCREEN_HEIGHT / 2 - 200;
for (var i = 0; i < targets.length; i++) {
var chip = targets[i];
if (chip.active) {
if (gameMode === 'coop') {
var isPlayerChip = chip.isPlayerSide;
var isPlayerBullet = self.isPlayerCard;
var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY;
if (!onHorizontalPath) {
if (isPlayerBullet !== isPlayerChip) {
continue;
}
}
}
var dx = chip.x - self.x;
var dy = chip.y - self.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < shortestDistanceSq) {
shortestDistanceSq = distanceSq;
bestTarget = chip;
}
}
}
return bestTarget;
};
self.findRicochetTarget = function () {
// Use the same targeting logic as cards, but from bullet's perspective
var targets;
if (gameMode === 'coop') {
targets = activePlayerChips.concat(activeAIChips);
} else {
targets = self.isPlayerCard ? activePlayerChips : activeAIChips;
}
var bestTarget = null;
var shortestDistanceSq = Infinity;
var turningPointY = SCREEN_HEIGHT / 2 - 200;
for (var i = 0; i < targets.length; i++) {
var potentialTarget = targets[i];
if (potentialTarget.active && self.ricochetedFrom.indexOf(potentialTarget) === -1) {
if (gameMode === 'coop') {
var isPlayerChip = potentialTarget.isPlayerSide;
var isPlayerBullet = self.isPlayerCard;
var onHorizontalPath = isPlayerChip ? potentialTarget.y <= turningPointY : potentialTarget.y >= turningPointY;
if (!onHorizontalPath) {
if (isPlayerBullet !== isPlayerChip) {
continue;
}
}
}
var dx = potentialTarget.x - self.x;
var dy = potentialTarget.y - self.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < shortestDistanceSq) {
shortestDistanceSq = distanceSq;
bestTarget = potentialTarget;
}
}
}
return bestTarget;
};
self.update = function () {
if (!self.active) {
return;
}
// If target is destroyed, try to find a new target instead of seeking last position
if (self.target && !self.target.active) {
var newTarget = null;
if (!self.hasRetargeted) {
newTarget = self.findNewTarget();
}
if (newTarget) {
self.target = newTarget;
self.targetLastX = newTarget.x;
self.targetLastY = newTarget.y;
self.hasRetargeted = true;
} else {
// No new target available, or already retargeted. Seek last position.
self.isSeekingLastPosition = true;
self.target = null;
}
}
// If seeking, move towards the last position
if (self.isSeekingLastPosition) {
var dx = self.targetLastX - self.x;
var dy = self.targetLastY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Arrived at destination, recycle bullet
self.isSeekingLastPosition = false;
if (currentGraphic) {
currentGraphic.visible = false;
}
PoolManager.returnBullet(self);
} else {
// Move towards destination
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
} else if (self.target) {
// Store previous position for continuous collision detection
var prevX = self.x;
var prevY = self.y;
// Update last known position
self.targetLastX = self.target.x;
self.targetLastY = self.target.y;
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate where bullet will be after this frame
var angle = Math.atan2(dy, dx);
var newX = self.x + Math.cos(angle) * self.speed;
var newY = self.y + Math.sin(angle) * self.speed;
// RELIABLE COLLISION DETECTION:
// Check if the bullet's path for this frame intersects the target's radius.
// This prevents "tunneling" where a fast bullet could pass through a target between frames.
var hitRadius = 85; // Visual radius of the chip, matches chip graphics
var collisionDistance = distancePointToLine(self.target.x, self.target.y, prevX, prevY, newX, newY);
if (collisionDistance <= hitRadius) {
// Hit detected
// NEW: Check for Execute mod for Spades BEFORE dealing damage
if (self.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'deathSpadesMod') {
// Execute threshold: starts at 15%, +1% per level. Max 40%.
var executeThreshold = Math.min(0.40, 0.15 + (self.level - 1) * 0.01);
// Execute chance: starts at 20%, +2% per level. Max 50%.
var executeChance = Math.min(0.50, 0.20 + (self.level - 1) * 0.02);
if (self.target.health / self.target.maxHealth <= executeThreshold) {
if (Math.random() < executeChance) {
// Execute!
createFloatingText('EXECUTE!', self.target.x, self.target.y - 60, 0xaa00aa, 50);
// Create the execution animation
var executionEffect = LK.getAsset('deathSpadesMod', {
anchorX: 0.5,
anchorY: 0.5,
x: self.target.x,
y: self.target.y,
scaleX: 0.1,
scaleY: 0.1,
alpha: 1
});
gameLayer.addChild(executionEffect);
tween(executionEffect, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 500,
easing: tween.quadOut,
onFinish: function onFinish() {
if (executionEffect.parent) {
executionEffect.parent.removeChild(executionEffect);
}
}
});
self.target.takeDamage(self.target.health, self.isPlayerCard); // Deal fatal damage
// Recycle bullet and stop further processing
if (currentGraphic) {
currentGraphic.visible = false;
}
PoolManager.returnBullet(self);
return; // IMPORTANT: Exit update for this bullet
}
}
}
// If not executed, proceed with normal hit logic
self.target.takeDamage(self.damage, self.isPlayerCard);
// NEW: Check if this is a hearts bullet and flame mod is equipped
if (self.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', self.isPlayerCard) === 'burnHeartMod') {
// Calculate burn damage (10% of hit damage per tick)
var burnDamage = self.damage * 0.1;
// Increase burn duration by 50% (from 5 to 7.5 ticks)
var burnDuration = Math.round(5 * 1.5); // 7.5 → 8 ticks = ~4 seconds of burning
// Apply burn effect
self.target.applyBurn(burnDamage, burnDuration, self.isPlayerCard);
}
// NEW: Check if this is a spades bullet and freeze mod is equipped
if (self.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'freezeSpadeMod') {
// Calculate freeze duration (3 seconds base)
var freezeDuration = 180; // 180 ticks at 60fps = 3 seconds
// Apply freeze effect
self.target.applyFreeze(freezeDuration);
}
// NEW: Check if this is a clubs bullet and slow mod is equipped
if (self.suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', self.isPlayerCard) === 'slowClubsMod') {
// Slow amount: 20% base, +5% per level. Max 70%.
var slowAmount = Math.min(0.7, 0.2 + (self.level - 1) * 0.05);
// Slow duration: 2s base, +0.5s per level.
var slowDuration = 120 + (self.level - 1) * 30; // 120 ticks = 2s
self.target.applySlow(slowAmount, slowDuration);
}
// NEW: Check for ricochet mod for clubs
if (self.ricochetCount > 0) {
self.ricochetedFrom.push(self.target);
var newTarget = self.findRicochetTarget();
if (newTarget) {
self.target = newTarget;
self.targetLastX = newTarget.x;
self.targetLastY = newTarget.y;
self.ricochetCount--;
self.damage = Math.floor(self.damage * 0.7); // 70% damage on next hit
// Visual feedback for ricochet
createExplosion(self.x, self.y, 0xcccccc);
return; // Keep the bullet alive and moving to the new target
}
}
if (currentGraphic) {
currentGraphic.visible = false;
}
PoolManager.returnBullet(self);
} else {
// No hit, move bullet forward
self.x = newX;
self.y = newY;
}
} else {
// No target and not seeking, recycle
if (currentGraphic) {
currentGraphic.visible = false;
}
PoolManager.returnBullet(self);
}
};
return self;
});
// Helper function to calculate distance from point to line segment
/****
* Object Pool Manager
****/
/****
* Card Class
****/
var Card = Container.expand(function (cardData) {
var self = Container.call(this);
self.cardData = cardData;
self.level = 1;
self.isInPlay = false;
self.isPlayerCard = true; // Track which player owns this card
self.playSlotX = 0;
self.playSlotY = 0;
self.lastFired = 0;
self.fireRate = 60; // Base fire rate (ticks between shots)
self.damage = 35; // Increased base damage
self.range = 200;
self.handBonus = 1;
self.timeOnBoard = 0;
self.lastIncomeTick = 0;
var redOutline = self.attachAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
redOutline.scale.set(1.1);
redOutline.alpha = 1.0;
redOutline.tint = 0xff0000; // Red color
redOutline.visible = false;
self.redOutline = redOutline;
var greenOutline = self.attachAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
greenOutline.scale.set(1.1);
greenOutline.alpha = 0.7;
greenOutline.tint = 0x00ff00;
greenOutline.visible = false;
self.greenOutline = greenOutline;
var cardGraphics = self.attachAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
// Define getSuitSymbol method before using it
self.getSuitSymbol = function (suit) {
switch (suit) {
case 'hearts':
return '♥';
case 'diamonds':
return '♦';
case 'clubs':
return '♣';
case 'spades':
return '♠';
case 'joker':
return '🃏';
default:
return '?';
}
};
// Card value in top left corner
if (cardData.suit !== 'joker') {
var valueText = new Text2(cardData.value, {
size: 56,
fill: CardSystem.suitColors[cardData.suit] || 0x000000,
weight: 800,
stroke: 0x000000,
strokeThickness: 0
});
valueText.anchor.set(0, 0);
valueText.x = -95; // Top left
valueText.y = -135;
self.addChild(valueText);
}
// Large suit symbol in center, deferred to activate()
var suitGraphics = null;
var jokerSuitGraphics = null;
// Level text at bottom
var levelText = new Text2('Lvl 1', {
size: 45,
fill: 0x000000,
weight: 800,
stroke: 0x000000,
strokeThickness: 0
});
levelText.anchor.set(0.5, 1);
levelText.y = 128; // Bottom of card
self.addChild(levelText);
self.activate = function (x, y, inPlay, isPlayerCard) {
self.x = x;
self.y = y;
self.isInPlay = inPlay || false;
self.isPlayerCard = isPlayerCard !== undefined ? isPlayerCard : true;
self.visible = true;
// Remove old graphics if they exist
if (suitGraphics && suitGraphics.parent) {
suitGraphics.parent.removeChild(suitGraphics);
suitGraphics = null;
}
if (jokerSuitGraphics && jokerSuitGraphics.parent) {
jokerSuitGraphics.parent.removeChild(jokerSuitGraphics);
jokerSuitGraphics = null;
}
// Create new suit graphic based on player/AI context
var suitAssetId = null;
var equippedMod = ModSystem.getEquippedModAsset(self.cardData.suit, self.isPlayerCard);
if (equippedMod) {
suitAssetId = equippedMod;
} else {
switch (self.cardData.suit) {
case 'hearts':
suitAssetId = 'heartSuit';
break;
case 'diamonds':
suitAssetId = 'diamondSuit';
break;
case 'clubs':
suitAssetId = 'clubSuit';
break;
case 'spades':
suitAssetId = 'spadeSuit';
break;
}
}
if (suitAssetId) {
suitGraphics = self.attachAsset(suitAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
suitGraphics.y = -15;
suitGraphics.scaleX = suitGraphics.scaleY = 0.8;
} else if (self.cardData.suit === 'joker') {
jokerSuitGraphics = self.attachAsset('jokerSuit', {
anchorX: 0.5,
anchorY: 0.5
});
jokerSuitGraphics.y = -15;
jokerSuitGraphics.scale.set(1.5);
}
if (inPlay) {
self.calculateStats();
}
};
self.calculateStats = function () {
// Stats based on level only, not card face value
var baseDamage = 10; // Reduced base damage
var baseFireRate = 60;
// Level scaling - more dramatic improvements per level
self.damage = Math.floor(baseDamage * Math.pow(1.6, self.level - 1)); // Further decreased scaling from 1.7 to 1.6
self.fireRate = Math.max(15, Math.floor(baseFireRate / Math.pow(1.2, self.level - 1))); // Reduced from 1.25 to 1.2
// Apply poker hand bonus
self.damage = Math.floor(self.damage * self.handBonus);
self.fireRate = Math.max(10, Math.floor(self.fireRate / self.handBonus));
// NEW: Apply mod-specific stat changes
if (self.cardData.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'mineSpadesMod') {
// Mine mod significantly slows fire rate in exchange for higher damage per mine.
self.fireRate *= 2.5;
}
};
self.setLevel = function (newLevel) {
if (self.cardData.suit === 'joker') {
self.level = 1;
levelText.visible = false;
self.calculateStats();
return;
}
self.level = newLevel;
levelText.setText('Lvl ' + self.level);
self.calculateStats();
self.resetInvestment();
// Visual feedback for higher levels
if (self.level > 1) {
// The glow effect was causing cards to become translucent.
// Level up is already indicated by animation and floating text.
}
};
self.resetInvestment = function () {
self.timeOnBoard = 0;
self.lastIncomeTick = LK.ticks;
};
self.canMergeWith = function (otherCard) {
if (!otherCard || otherCard === self) {
return false;
}
// If the card being dropped onto is a Joker, it cannot be leveled up.
if (otherCard.cardData && otherCard.cardData.suit === 'joker') {
return false;
}
// If the card being dragged is a Joker, it can merge with any non-Joker card.
if (self.cardData.suit === 'joker') {
return true;
}
// Must be same level AND (same suit OR same value)
var sameLevel = self.level === otherCard.level;
var sameSuit = self.cardData.suit === otherCard.cardData.suit;
var sameValue = self.cardData.value === otherCard.cardData.value;
return sameLevel && (sameSuit || sameValue);
};
self.mergeWith = function (otherCard) {
if (!self.canMergeWith(otherCard)) {
return null;
}
// When merging, the new card levels up. The new level is one higher than the card on the board.
var newLevel = otherCard.level + 1;
// Special case: If the card being dragged is a Joker, the target card just increases its level
if (self.cardData.suit === 'joker') {
var mergedCard = new Card(otherCard.cardData);
mergedCard.setLevel(newLevel);
return mergedCard;
}
// The new card is of a random type, regardless of what was merged.
var randomSuit = CardSystem.suits[Math.floor(Math.random() * CardSystem.suits.length)];
var randomValue = CardSystem.values[Math.floor(Math.random() * CardSystem.values.length)];
var newCardData = {
suit: randomSuit,
value: randomValue,
id: randomSuit + '_' + randomValue
};
var mergedCard = new Card(newCardData);
mergedCard.setLevel(newLevel);
return mergedCard;
};
self.findTarget = function () {
// Player cards target enemies attacking the player (activePlayerChips)
// AI cards target enemies attacking the AI (activeAIChips)
var targets;
if (gameMode === 'coop') {
// In coop, start with all targets and filter based on position
targets = activePlayerChips.concat(activeAIChips);
} else {
targets = self.isPlayerCard ? activePlayerChips : activeAIChips;
}
var bestTarget = null;
var highestProgress = -1;
var turningPointY = SCREEN_HEIGHT / 2 - 200;
for (var i = 0; i < targets.length; i++) {
var chip = targets[i];
// Add extra validation for alive targets
if (chip.active && chip.health > 0 && chip.visible && chip.pathProgress > highestProgress) {
if (gameMode === 'coop') {
var isPlayerChip = chip.isPlayerSide;
var isPlayerCard = self.isPlayerCard;
// Player chips move up (y decreases), AI chips move down (y increases).
var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY;
if (!onHorizontalPath) {
// Not on the shared horizontal path yet, so cards can only target their own side's enemies.
if (isPlayerCard !== isPlayerChip) {
continue;
}
}
// If onHorizontalPath is true, any card can target the chip, so no 'continue' needed.
}
highestProgress = chip.pathProgress;
bestTarget = chip;
}
}
return bestTarget;
};
self.fire = function () {
var target = self.findTarget();
if (!target) {
return;
}
// NEW: Handle Mine Spades Mod
if (self.cardData.suit === 'spades' && ModSystem.getEquippedModAsset('spades', self.isPlayerCard) === 'mineSpadesMod') {
var mine = PoolManager.getMine(self.isPlayerCard);
if (mine) {
var pathProgress = Math.random() * 70 + 15;
var minePos = PathSystem.getPositionAlongPath(pathProgress, self.isPlayerCard);
var mineDamage = Math.floor(self.damage * 2.5); // Mines deal 2.5x card damage
mine.activate(minePos.x, minePos.y, mineDamage, self.isPlayerCard);
gameLayer.addChild(mine);
activeMines.push(mine);
}
} else {
var damageToDeal = self.damage;
// NEW: Calculate Unity Bonus for Hearts
if (self.cardData.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', self.isPlayerCard) === 'unityHeartsMod') {
var playArea = self.isPlayerCard ? gameState.playerPlayArea : gameState.aiPlayArea;
var cardRow = -1,
cardCol = -1;
// Find card's position in the grid
for (var r = 0; r < PLAY_AREA_ROWS; r++) {
for (var c = 0; c < PLAY_AREA_COLS; c++) {
if (playArea[r][c] === self) {
cardRow = r;
cardCol = c;
break;
}
}
if (cardRow !== -1) {
break;
}
}
if (cardRow !== -1) {
var adjacentHearts = 0;
var neighbors = [[cardRow - 1, cardCol], [cardRow + 1, cardCol], [cardRow, cardCol - 1], [cardRow, cardCol + 1]];
for (var i = 0; i < neighbors.length; i++) {
var nRow = neighbors[i][0];
var nCol = neighbors[i][1];
if (nRow >= 0 && nRow < PLAY_AREA_ROWS && nCol >= 0 && nCol < PLAY_AREA_COLS) {
var neighborCard = playArea[nRow][nCol];
if (neighborCard && neighborCard.cardData.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', neighborCard.isPlayerCard) === 'unityHeartsMod') {
adjacentHearts++;
}
}
}
// Each adjacent heart adds 25% bonus damage
damageToDeal = Math.floor(damageToDeal * (1 + 0.25 * adjacentHearts));
}
}
// NEW: Calculate Gambler Bonus for Diamonds
if (self.cardData.suit === 'diamonds' && ModSystem.getEquippedModAsset('diamonds', self.isPlayerCard) === 'gamblerDiamondsMod') {
// Base 50% chance of double damage, increases by 5% per level. Capped at 95%.
var doubleDamageChance = Math.min(0.95, 0.5 + (self.level - 1) * 0.05);
if (Math.random() < doubleDamageChance) {
damageToDeal *= 2;
createFloatingText('x2!', self.x, self.y - 70, 0xffd700, 30);
} else {
damageToDeal = 0;
createFloatingText('Miss!', self.x, self.y - 70, 0xaaaaaa, 30);
}
}
// NEW: Calculate Boost from adjacent Diamonds
var playArea = self.isPlayerCard ? gameState.playerPlayArea : gameState.aiPlayArea;
var cardRow = -1,
cardCol = -1;
// Find card's position in the grid
for (var r = 0; r < PLAY_AREA_ROWS; r++) {
for (var c = 0; c < PLAY_AREA_COLS; c++) {
if (playArea[r][c] === self) {
cardRow = r;
cardCol = c;
break;
}
}
if (cardRow !== -1) {
break;
}
}
if (cardRow !== -1) {
var totalBoostPercentage = 0;
var neighbors = [[cardRow - 1, cardCol], [cardRow + 1, cardCol], [cardRow, cardCol - 1], [cardRow, cardCol + 1]];
for (var i = 0; i < neighbors.length; i++) {
var nRow = neighbors[i][0];
var nCol = neighbors[i][1];
if (nRow >= 0 && nRow < PLAY_AREA_ROWS && nCol >= 0 && nCol < PLAY_AREA_COLS) {
var neighborCard = playArea[nRow][nCol];
if (neighborCard && neighborCard.cardData.suit === 'diamonds' && ModSystem.getEquippedModAsset('diamonds', neighborCard.isPlayerCard) === 'boostDiamondsMod') {
// Each level of the booster card adds 10% damage.
totalBoostPercentage += neighborCard.level * 0.10;
}
}
}
if (totalBoostPercentage > 0) {
damageToDeal = Math.floor(damageToDeal * (1 + totalBoostPercentage));
}
}
// Check if this is a clubs card with spreadshot mod equipped
var isSpreadshot = self.cardData.suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', self.isPlayerCard) === 'spreadClubMod';
if (isSpreadshot) {
// Spreadshot: reduced damage but hits multiple targets
var spreadDamage = Math.floor(self.damage * 0.6); // 30% damage reduction
var maxTargets = Math.min(1 + self.level, 5); // 1 + level targets, max 5
var allTargets;
if (gameMode === 'coop') {
allTargets = activePlayerChips.concat(activeAIChips);
} else {
allTargets = self.isPlayerCard ? activePlayerChips : activeAIChips;
}
var closestTargets = []; // This will store {chip, distanceSq} objects
var turningPointY = SCREEN_HEIGHT / 2 - 200;
// Find multiple targets using an efficient selection method instead of sorting all targets
for (var i = 0; i < allTargets.length; i++) {
var chip = allTargets[i];
if (chip.active && chip.health > 0 && chip.visible) {
if (gameMode === 'coop') {
var isPlayerChip = chip.isPlayerSide;
var isPlayerCard = self.isPlayerCard;
var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY;
if (!onHorizontalPath) {
if (isPlayerCard !== isPlayerChip) {
continue;
}
}
}
var dx = chip.x - self.x;
var dy = chip.y - self.y;
var distanceSq = dx * dx + dy * dy;
if (closestTargets.length < maxTargets) {
closestTargets.push({
chip: chip,
distanceSq: distanceSq
});
} else {
var furthestDistSq = -1;
var furthestIdx = -1;
for (var j = 0; j < closestTargets.length; j++) {
if (closestTargets[j].distanceSq > furthestDistSq) {
furthestDistSq = closestTargets[j].distanceSq;
furthestIdx = j;
}
}
if (distanceSq < furthestDistSq) {
closestTargets[furthestIdx] = {
chip: chip,
distanceSq: distanceSq
};
}
}
}
}
// Fire at the found targets
for (var i = 0; i < closestTargets.length; i++) {
var bullet = PoolManager.getBullet();
if (bullet) {
bullet.activate(self.x, self.y, closestTargets[i].chip, spreadDamage, self.cardData.suit, self.isPlayerCard, self.level);
gameLayer.addChild(bullet);
activeBullets.push(bullet);
}
}
} else {
// Normal single-target firing
var bullet = PoolManager.getBullet();
if (bullet) {
bullet.activate(self.x, self.y, target, damageToDeal, self.cardData.suit, self.isPlayerCard, self.level);
gameLayer.addChild(bullet);
activeBullets.push(bullet);
}
}
}
// Play shoot sound for player cards
if (self.isPlayerCard) {
LK.getSound('shootSound').play();
}
self.lastFired = LK.ticks;
// Visual feedback for firing (enhanced for spreadshot)
tween.stop(self, {
scaleX: true,
scaleY: true
});
var isSpreadshot = self.cardData.suit === 'clubs' && ModSystem.getEquippedModAsset('clubs', self.isPlayerCard) === 'spreadClubMod';
var scaleAmount = isSpreadshot ? 0.8 : 0.9; // More dramatic scale for spreadshot
tween(self, {
scaleX: scaleAmount,
scaleY: scaleAmount
}, {
duration: 100,
easing: tween.quadOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 150,
easing: tween.elasticOut
});
}
});
};
self.update = function () {
if (!self.isInPlay) {
return;
}
// NEW: Handle Investment Hearts Mod
if (self.cardData.suit === 'hearts' && ModSystem.getEquippedModAsset('hearts', self.isPlayerCard) === 'investmentHeartsMod') {
// Generate income every 5 seconds (300 ticks)
var incomeInterval = 300;
if (LK.ticks - self.lastIncomeTick >= incomeInterval) {
self.lastIncomeTick = LK.ticks;
// timeOnBoard is a counter of income ticks, not actual ticks on board
self.timeOnBoard++;
// Income formula: 1 chip base, +1 for every 2 intervals (10 seconds). Max 5.
var income = 1 + Math.floor(self.timeOnBoard / 2);
income = Math.min(income, 5); // Cap income
if (self.isPlayerCard) {
gameState.playerChips += income;
createFloatingText('+' + income, self.x, self.y - 70, 0xffd700, 30);
updateUI();
} else {
// AI gets income too
gameState.aiChips += income;
}
}
}
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
}
};
// Initialize with level 1
self.setLevel(1);
return self;
});
var Mine = Container.expand(function () {
var self = Container.call(this);
self.active = false;
self.damage = 50;
self.isPlayerMine = true;
self.explosionRadiusSq = 150 * 150;
var mineGraphics = self.attachAsset('mine', {
anchorX: 0.5,
anchorY: 0.5
});
mineGraphics.scale.set(0.8);
self.activate = function (x, y, damage, isPlayerMine) {
self.active = true;
self.visible = true;
self.x = x;
self.y = y;
self.damage = damage;
self.isPlayerMine = isPlayerMine;
self.scale.set(0.1);
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.backOut
});
};
self.explode = function () {
if (!self.active) {
return;
}
self.active = false;
createExplosion(self.x, self.y, 0xffa500);
var targets;
if (gameMode === 'coop') {
targets = activePlayerChips.concat(activeAIChips);
} else {
targets = self.isPlayerMine ? activePlayerChips : activeAIChips;
}
var turningPointY = SCREEN_HEIGHT / 2 - 200;
for (var i = targets.length - 1; i >= 0; i--) {
var chip = targets[i];
if (chip.active) {
if (gameMode === 'coop') {
var isPlayerChip = chip.isPlayerSide;
var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY;
if (!onHorizontalPath) {
if (self.isPlayerMine !== isPlayerChip) {
continue;
}
}
}
var dx = chip.x - self.x;
var dy = chip.y - self.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < self.explosionRadiusSq) {
chip.takeDamage(self.damage, self.isPlayerMine);
}
}
}
PoolManager.returnMine(self);
};
self.update = function () {
if (!self.active) {
return;
}
var targets;
if (gameMode === 'coop') {
targets = activePlayerChips.concat(activeAIChips);
} else {
targets = self.isPlayerMine ? activePlayerChips : activeAIChips;
}
var collisionRadiusSq = 45 * 45;
var turningPointY = SCREEN_HEIGHT / 2 - 200;
for (var i = 0; i < targets.length; i++) {
var chip = targets[i];
if (chip.active) {
if (gameMode === 'coop') {
var isPlayerChip = chip.isPlayerSide;
var onHorizontalPath = isPlayerChip ? chip.y <= turningPointY : chip.y >= turningPointY;
if (!onHorizontalPath) {
if (self.isPlayerMine !== isPlayerChip) {
continue;
}
}
}
var dx = chip.x - self.x;
var dy = chip.y - self.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < collisionRadiusSq) {
self.explode();
break;
}
}
}
};
return self;
});
/****
* Poker Chip Enemy Class
****/
var PokerChip = Container.expand(function () {
var self = Container.call(this);
self.active = false;
self.health = 100; // Increased base health
self.maxHealth = 100;
self.value = 1;
self.speed = 0.05;
self.pathProgress = 0;
self.isPlayerSide = true;
self.damageFlashTimer = 0;
self.burnDamage = 0;
self.burnDuration = 0;
self.burnTickTimer = 0;
self.burnSourceIsPlayer = null;
self.burnTickInterval = 30; // Burn ticks every 0.5 seconds (30 ticks at 60fps)
self.slowDuration = 0;
self.slowAmount = 0;
self.freezeDuration = 0;
self.originalSpeed = 0;
self.iceCube = null; // Will hold the ice cube graphic
self.freezeImmunityTimer = 0;
self.slowEffectIcon = null;
var chipGraphicsAssets = {
1: self.attachAsset('yellowChip', {
anchorX: 0.5,
anchorY: 0.5
}),
5: self.attachAsset('redChip', {
anchorX: 0.5,
anchorY: 0.5
}),
10: self.attachAsset('greenChip', {
anchorX: 0.5,
anchorY: 0.5
}),
25: self.attachAsset('blueChip', {
anchorX: 0.5,
anchorY: 0.5
}),
100: self.attachAsset('purpleChip', {
anchorX: 0.5,
anchorY: 0.5
})
};
var chipGraphics = null; // This will hold the current visible chip
for (var val in chipGraphicsAssets) {
chipGraphicsAssets[val].visible = false;
}
var healthText = new Text2('', {
size: 80,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 8
});
healthText.anchor.set(0.5, 0.5);
self.addChild(healthText);
self.applyBurn = function (damage, duration, isPlayerSource) {
// Stack burn damage but refresh duration
self.burnDamage += damage;
self.burnDuration = Math.max(self.burnDuration, duration);
self.burnTickTimer = 0;
if (isPlayerSource !== undefined) {
self.burnSourceIsPlayer = isPlayerSource;
}
// REMOVED: Visual tint feedback - only use particles now
// if (chipGraphics) {
// chipGraphics.tint = 0xff4400; // Orange tint for burning
// }
// Create burn effect particles
createBurnEffect(self.x, self.y);
};
self.applyFreeze = function (duration) {
// Return if chip is immune to freeze
if (self.freezeImmunityTimer > 0) {
return;
}
// Don't freeze if already frozen - with special logic for bosses
if (self.freezeDuration > 0) {
if (!self.isBoss) {
// Non-bosses can refresh duration
self.freezeDuration = Math.max(self.freezeDuration, duration);
}
// Bosses simply ignore the new freeze request if they are already frozen.
return;
}
self.freezeDuration = duration;
// Store original speed and stop movement
if (self.slowDuration <= 0) {
self.originalSpeed = self.speed;
}
self.speed = 0;
// Create ice cube encasement
if (!self.iceCube) {
self.iceCube = LK.getAsset('iceCube', {
anchorX: 0.5,
anchorY: 0.5
});
self.iceCube.scale.set(1.2); // Slightly larger than chip
self.iceCube.alpha = 0.8;
self.addChild(self.iceCube);
}
self.iceCube.visible = true;
// Ice formation animation
self.iceCube.scale.set(0.1);
self.iceCube.alpha = 0;
tween(self.iceCube, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 300,
easing: tween.backOut
});
};
self.processFreeze = function () {
// Process immunity first, which ticks down every frame.
if (self.freezeImmunityTimer > 0) {
self.freezeImmunityTimer--;
}
if (self.freezeDuration <= 0) {
return;
}
self.freezeDuration--;
// Create occasional ice sparkle effects
if (Math.random() < 0.1) {
createIceSparkles(self.x, self.y);
}
// When freeze expires, break the ice
if (self.freezeDuration <= 0) {
if (self.slowDuration > 0) {
self.speed = self.originalSpeed * (1 - self.slowAmount);
} else {
self.speed = self.originalSpeed;
}
self.freezeImmunityTimer = 120; // Apply 2-second immunity (120 ticks)
if (self.iceCube) {
// Ice breaking animation
tween(self.iceCube, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 200,
easing: tween.quadOut,
onFinish: function onFinish() {
self.iceCube.visible = false;
}
});
// Create ice shatter particles
createIceShatterEffect(self.x, self.y);
}
}
};
self.processBurnDamage = function () {
if (self.burnDuration <= 0) {
// Ensure burn is completely cleared
self.burnDamage = 0;
self.burnTickTimer = 0;
return;
}
self.burnTickTimer++;
if (self.burnTickTimer >= self.burnTickInterval) {
self.burnTickTimer = 0;
self.burnDuration--;
// Apply burn damage (10% of original hit damage)
var actualBurnDamage = Math.ceil(self.burnDamage);
if (gameMode === 'coop' && self.burnSourceIsPlayer !== null) {
var damageAmount = Math.min(actualBurnDamage, self.health);
if (self.burnSourceIsPlayer) {
playerTotalDamage += damageAmount;
} else {
aiTotalDamage += damageAmount;
}
}
self.health -= actualBurnDamage;
self.updateHealthText();
// Create floating burn damage text
createFloatingText('-' + actualBurnDamage, self.x + (Math.random() - 0.5) * 40, self.y - 30, 0xff4400, 30);
// Burn particles
createBurnEffect(self.x, self.y);
if (self.health <= 0) {
self.active = false;
self.die();
return;
}
// Reduce burn damage over time (burn weakens)
self.burnDamage *= 0.9;
// Clear burn when duration expires - NO TINT MANAGEMENT
if (self.burnDuration <= 0) {
self.burnDamage = 0;
self.burnTickTimer = 0; // Also reset tick timer
// REMOVED: No tint reset needed
// if (chipGraphics && self.damageFlashTimer <= 0) {
// chipGraphics.tint = 0xffffff;
// }
}
}
};
self.applySlow = function (amount, duration) {
// A new slow can be applied if it's stronger, or to refresh duration.
// We won't let slow override freeze.
if (self.freezeDuration > 0) {
return;
}
if (self.slowDuration <= 0) {
// Not currently slowed, so store the original speed
self.originalSpeed = self.speed;
// Apply visual effects
if (chipGraphics) {
chipGraphics.tint = 0xaaaaaa;
}
if (!self.slowEffectIcon) {
self.slowEffectIcon = LK.getAsset('slowClubsMod', {
anchorX: 0.5,
anchorY: 0.5
});
self.slowEffectIcon.scale.set(0.7);
self.addChild(self.slowEffectIcon);
}
self.slowEffectIcon.visible = true;
}
self.slowDuration = duration;
self.slowAmount = amount;
// Apply slow relative to the original speed before any slows were applied.
self.speed = self.originalSpeed * (1 - self.slowAmount);
};
self.processSlow = function () {
if (self.slowDuration > 0) {
self.slowDuration--;
if (self.slowDuration <= 0) {
self.slowAmount = 0;
// Slow has expired. If not frozen, restore speed.
if (self.freezeDuration <= 0) {
self.speed = self.originalSpeed;
}
// Remove visual effects
if (chipGraphics && self.damageFlashTimer <= 0) {
chipGraphics.tint = 0xffffff;
}
if (self.slowEffectIcon) {
self.slowEffectIcon.visible = false;
}
}
}
};
self.activate = function (value, isPlayerSide, startPos) {
self.active = true;
self.visible = true;
self.value = value;
self.isPlayerSide = isPlayerSide;
// Mark boss chips (they have much higher values than normal chips)
self.isBoss = value >= 100;
// Health scales with chip value - more valuable chips are tankier
self.maxHealth = value * 50;
// Health scaling per wave - NOW UNIFIED FOR BOTH MODES
// Increase health by 50% after each boss/mini-boss wave (every 5 waves)
var healthMultiplier = Math.pow(1.5, Math.floor((WaveSystem.waveNumber - 1) / 5));
self.maxHealth = self.maxHealth * healthMultiplier;
// In both modes, bosses have 30% less health to keep them manageable
if (self.isBoss) {
self.maxHealth *= 0.7;
// In PvP, bosses and mini-bosses get 20% more health
if (typeof gameMode !== 'undefined' && gameMode === 'pvp') {
self.maxHealth *= 1.2;
}
}
self.health = self.maxHealth;
self.pathProgress = 0;
// NEW: Reset burn status to ensure clean state
self.burnDamage = 0;
self.burnDuration = 0;
self.burnTickTimer = 0;
self.burnSourceIsPlayer = null;
// NEW: Reset freeze status to ensure clean state
self.slowDuration = 0;
self.slowAmount = 0;
self.freezeDuration = 0;
self.originalSpeed = 0;
if (self.iceCube) {
self.iceCube.visible = false;
}
if (self.slowEffectIcon) {
self.slowEffectIcon.visible = false;
}
self.freezeImmunityTimer = 0;
// Set all chips to yellow chip speed
self.speed = 0.03;
// Reset damage flash timer
self.damageFlashTimer = 0;
// Recreate healthText to ensure proper styling
if (healthText && healthText.parent) {
healthText.parent.removeChild(healthText);
}
healthText = new Text2('', {
size: 80,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 8
});
healthText.anchor.set(0.5, 0.5);
self.addChild(healthText);
self.setChipAppearance();
self.x = startPos.x;
self.y = startPos.y;
};
self.updateHealthText = function () {
// Remove the old text object
if (healthText && healthText.parent) {
healthText.parent.removeChild(healthText);
}
// Create a new text object with all styling properties
healthText = new Text2(formatNumberWithSuffix(Math.max(0, self.health)), {
size: 80,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 8
});
healthText.anchor.set(0.5, 0.5);
self.addChild(healthText);
};
self.setChipAppearance = function () {
// Hide previous graphic and reset its scale to default if it exists
if (chipGraphics) {
chipGraphics.visible = false;
chipGraphics.scale.set(1);
}
chipGraphics = chipGraphicsAssets[self.value] || chipGraphicsAssets[1];
if (chipGraphics) {
chipGraphics.visible = true;
// Apply boss scaling
if (self.isBoss) {
chipGraphics.scale.set(1.25);
}
}
self.updateHealthText();
};
self.takeDamage = function (damage, isPlayerSource) {
if (damage > 0) {
createFloatingText('-' + Math.round(damage), self.x + (Math.random() - 0.5) * 40, self.y - 60, 0xff0000, 50);
}
if (gameMode === 'coop' && isPlayerSource !== undefined) {
var damageAmount = Math.min(damage, self.health);
if (isPlayerSource) {
playerTotalDamage += damageAmount;
} else {
aiTotalDamage += damageAmount;
}
}
self.health -= damage;
self.updateHealthText();
self.damageFlashTimer = 10; // Flash for 10 ticks instead of setTimeout
if (self.health <= 0) {
// IMMEDIATELY mark as inactive to prevent further hits
self.active = false;
// Call kill counter handler before die() removes chip
handleChipKill(self.isPlayerSide);
self.die();
}
};
self.die = function () {
// Sound effect removed - no longer play pokerChipDie sound
var chipsEarned = Math.ceil(self.value * 1.5);
if (self.isPlayerSide) {
var greedBonus = calculateGreedBonus(true, chipsEarned);
var totalEarned = chipsEarned + greedBonus;
gameState.playerChips += totalEarned;
if (greedBonus > 0) {
createFloatingText('+' + formatNumberWithSuffix(totalEarned) + ' (+' + formatNumberWithSuffix(greedBonus) + ' greed)', self.x, self.y - 30, 0xffd700, 35);
}
} else {
var greedBonus = calculateGreedBonus(false, chipsEarned);
gameState.aiChips += chipsEarned + greedBonus;
}
PoolManager.returnChip(self);
};
self.update = function () {
if (!self.active) {
return;
}
// Handle damage flash (keep this for regular damage)
if (self.damageFlashTimer > 0) {
self.damageFlashTimer--;
if (chipGraphics) {
if (self.damageFlashTimer > 0) {
chipGraphics.tint = 0xff0000;
} else {
// Flash is over, restore correct tint
if (self.slowDuration > 0) {
chipGraphics.tint = 0xaaaaaa;
} else {
chipGraphics.tint = 0xffffff;
}
}
}
}
// NEW: Process burn damage
self.processBurnDamage();
// NEW: Process freeze effect
self.processFreeze();
// NEW: Process slow effect
self.processSlow();
if (!self.active) {
return;
}
self.pathProgress += self.speed;
var pathPos = PathSystem.getPositionAlongPath(self.pathProgress, self.isPlayerSide);
var isDefeated = pathPos.completed;
if (!isDefeated && gameMode === 'coop') {
// In coop mode, enemies are also defeated if they pass the right edge of the background.
var rightEdge = coopBackground.x + coopBackground.width / 2;
if (pathPos.x > rightEdge) {
isDefeated = true;
}
}
if (isDefeated) {
// Boss chips remove 2 hearts instead of 1
var heartsToRemove = self.isBoss ? 2 : 1;
if (gameMode === 'coop') {
gameState.playerLives -= heartsToRemove;
gameState.aiLives = gameState.playerLives;
} else {
if (self.isPlayerSide) {
gameState.playerLives -= heartsToRemove;
} else {
gameState.aiLives -= heartsToRemove;
}
}
LK.effects.flashScreen(0xff0000, 300);
PoolManager.returnChip(self);
return;
}
self.x = pathPos.x;
self.y = pathPos.y;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0f3d0f
});
/****
* Game Code
****/
/****
* Game Constants
****/
/****
* Poker Tower Defense - Complete Refactor
****/
// Helper function to calculate distance from point to line segment
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray2(r) || _nonIterableSpread();
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _unsupportedIterableToArray2(r, a) {
if (r) {
if ("string" == typeof r) {
return _arrayLikeToArray2(r, a);
}
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray2(r, a) : void 0;
}
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) {
return Array.from(r);
}
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) {
return _arrayLikeToArray2(r);
}
}
function _arrayLikeToArray2(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) {
n[e] = r[e];
}
return n;
}
function distancePointToLine(px, py, x1, y1, x2, y2) {
var A = px - x1;
var B = py - y1;
var C = x2 - x1;
var D = y2 - y1;
var dot = A * C + B * D;
var lenSq = C * C + D * D;
if (lenSq === 0) {
// Line segment is actually a point
return Math.sqrt(A * A + B * B);
}
var param = dot / lenSq;
var xx, yy;
if (param < 0) {
xx = x1;
yy = y1;
} else if (param > 1) {
xx = x2;
yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
var dx = px - xx;
var dy = py - yy;
return Math.sqrt(dx * dx + dy * dy);
}
function _createForOfIteratorHelper(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (!t) {
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
t && (r = t);
var _n = 0,
F = function F() {};
return {
s: F,
n: function n() {
return _n >= r.length ? {
done: !0
} : {
done: !1,
value: r[_n++]
};
},
e: function e(r) {
throw r;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var o,
a = !0,
u = !1;
return {
s: function s() {
t = t.call(r);
},
n: function n() {
var r = t.next();
return a = r.done, r;
},
e: function e(r) {
u = !0, o = r;
},
f: function f() {
try {
a || null == t["return"] || t["return"]();
} finally {
if (u) {
throw o;
}
}
}
};
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) {
return _arrayLikeToArray(r, a);
}
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) {
n[e] = r[e];
}
return n;
}
var SCREEN_WIDTH = 2048;
var SCREEN_HEIGHT = 2732;
var PLAY_AREA_COLS = 5;
var PLAY_AREA_ROWS = 2;
var SLOT_WIDTH = 300;
var SLOT_HEIGHT = 420;
var DEAL_SLOT_WIDTH = 240;
var DEAL_SLOT_HEIGHT = 330;
// AI area positioning (top)
var AI_AREA_X = (SCREEN_WIDTH - PLAY_AREA_COLS * SLOT_WIDTH) / 2;
var AI_AREA_Y = 150;
// Player area positioning (middle, with plenty of room below)
var PLAYER_AREA_X = (SCREEN_WIDTH - PLAY_AREA_COLS * SLOT_WIDTH) / 2;
var PLAYER_AREA_Y = SCREEN_HEIGHT - 1300; // Much higher up
// Player deal area (hand slots) - below play area
var PLAYER_DEAL_AREA_Y = PLAYER_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT + 100;
/****
* Card System
****/
var CardSystem = {
suits: ['hearts', 'diamonds', 'clubs', 'spades'],
values: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
suitColors: {
'hearts': 0xff0000,
'diamonds': 0xff0000,
'clubs': 0x000000,
'spades': 0x000000
},
createDeck: function createDeck() {
var deck = [];
var _iterator = _createForOfIteratorHelper(this.suits),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var suit = _step.value;
var _iterator2 = _createForOfIteratorHelper(this.values),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var value = _step2.value;
deck.push({
suit: suit,
value: value,
id: suit + '_' + value
});
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
}
// Add jokers
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
deck.push({
suit: 'joker',
value: 'red',
id: 'joker_red'
});
deck.push({
suit: 'joker',
value: 'black',
id: 'joker_black'
});
return this.shuffleDeck(deck);
},
shuffleDeck: function shuffleDeck(deck) {
for (var i = deck.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;
}
return deck;
},
getCardValue: function getCardValue(card) {
if (card.cardData.suit === 'joker') {
return 14;
} // Jokers are highest
if (card.cardData.value === 'A') {
return 14;
} // Aces high
if (card.cardData.value === 'K') {
return 13;
}
if (card.cardData.value === 'Q') {
return 12;
}
if (card.cardData.value === 'J') {
return 11;
}
return parseInt(card.cardData.value);
},
evaluatePokerHand: function evaluatePokerHand(cards) {
var _this = this;
if (!cards || cards.length === 0) {
return {
type: 'none',
strength: 0,
multiplier: 1,
contributingCards: []
};
}
// Sort cards by value for easier analysis
var sortedCards = cards.slice().sort(function (a, b) {
return _this.getCardValue(b) - _this.getCardValue(a);
});
var values = sortedCards.map(function (card) {
return _this.getCardValue(card);
});
var suits = sortedCards.map(function (card) {
return card.cardData.suit;
});
// Count values and suits
var valueCounts = {};
var suitCounts = {};
values.forEach(function (value) {
return valueCounts[value] = (valueCounts[value] || 0) + 1;
});
suits.forEach(function (suit) {
return suitCounts[suit] = (suitCounts[suit] || 0) + 1;
});
var counts = Object.values(valueCounts).sort(function (a, b) {
return b - a;
});
// Only check for 5-card hands if we have 5 cards
var isFlush = false;
var isStraight = false;
if (cards.length === 5) {
isFlush = Object.keys(suitCounts).length === 1;
isStraight = this.checkStraight(values);
}
// Royal Flush
if (isFlush && isStraight && values[0] === 14 && values[4] === 10) {
return {
type: 'royal_flush',
strength: 10,
multiplier: 25,
contributingCards: sortedCards
};
}
// Straight Flush
if (isFlush && isStraight) {
return {
type: 'straight_flush',
strength: 9,
multiplier: 12,
contributingCards: sortedCards
};
}
// Four of a Kind
if (counts[0] >= 4) {
var quadValue;
for (var v in valueCounts) {
if (valueCounts[v] >= 4) {
quadValue = parseInt(v);
break;
}
}
return {
type: 'four_of_a_kind',
strength: 8,
multiplier: 8,
contributingCards: sortedCards.filter(function (c) {
return _this.getCardValue(c) === quadValue;
})
};
}
// Full House
if (counts[0] === 3 && counts[1] === 2) {
return {
type: 'full_house',
strength: 7,
multiplier: 5,
contributingCards: sortedCards
};
}
// Flush
if (isFlush) {
return {
type: 'flush',
strength: 6,
multiplier: 3.5,
contributingCards: sortedCards
};
}
// Straight
if (isStraight) {
return {
type: 'straight',
strength: 5,
multiplier: 2.5,
contributingCards: sortedCards
};
}
// Three of a Kind
if (counts[0] === 3) {
var tripValue;
for (var v in valueCounts) {
if (valueCounts[v] === 3) {
tripValue = parseInt(v);
break;
}
}
return {
type: 'three_of_a_kind',
strength: 4,
multiplier: 2,
contributingCards: sortedCards.filter(function (c) {
return _this.getCardValue(c) === tripValue;
})
};
}
// Two Pair
if (counts[0] === 2 && counts[1] === 2) {
var pairValues = [];
for (var v in valueCounts) {
if (valueCounts[v] === 2) {
pairValues.push(parseInt(v));
}
}
return {
type: 'two_pair',
strength: 3,
multiplier: 1.5,
contributingCards: sortedCards.filter(function (c) {
return pairValues.indexOf(_this.getCardValue(c)) !== -1;
})
};
}
// One Pair
if (counts[0] === 2) {
var pairValue;
for (var v in valueCounts) {
if (valueCounts[v] === 2) {
pairValue = parseInt(v);
break;
}
}
return {
type: 'one_pair',
strength: 2,
multiplier: 1.2,
contributingCards: sortedCards.filter(function (c) {
return _this.getCardValue(c) === pairValue;
})
};
}
// High Card
return {
type: 'high_card',
strength: 1,
multiplier: 1,
contributingCards: [sortedCards[0]]
};
},
checkStraight: function checkStraight(values) {
if (values.length !== 5) {
return false;
}
// Check for ace-low straight (A, 2, 3, 4, 5)
if (values[0] === 14 && values[1] === 5 && values[2] === 4 && values[3] === 3 && values[4] === 2) {
return true;
}
// Check normal straight
for (var i = 0; i < 4; i++) {
if (values[i] - values[i + 1] !== 1) {
return false;
}
}
return true;
}
};
/****
* Object Pool Manager
****/
var PoolManager = {
chipPool: [],
bulletPool: [],
cardPool: [],
playerMinePool: [],
aiMinePool: [],
CHIP_POOL_SIZE: 50,
BULLET_POOL_SIZE: 100,
CARD_POOL_SIZE: 60,
MINE_POOL_SIZE: 30,
// 30 per side
init: function init() {
// Initialize pools
for (var i = 0; i < this.CHIP_POOL_SIZE; i++) {
var chip = new PokerChip();
chip.active = false;
chip.visible = false;
this.chipPool.push(chip);
}
for (var i = 0; i < this.BULLET_POOL_SIZE; i++) {
var bullet = new Bullet();
bullet.active = false;
bullet.visible = false;
this.bulletPool.push(bullet);
}
for (var i = 0; i < this.MINE_POOL_SIZE; i++) {
var playerMine = new Mine();
playerMine.active = false;
playerMine.visible = false;
this.playerMinePool.push(playerMine);
var aiMine = new Mine();
aiMine.active = false;
aiMine.visible = false;
this.aiMinePool.push(aiMine);
}
},
getChip: function getChip() {
for (var i = 0; i < this.chipPool.length; i++) {
if (!this.chipPool[i].active) {
return this.chipPool[i];
}
}
return null;
},
getBullet: function getBullet() {
for (var i = 0; i < this.bulletPool.length; i++) {
if (!this.bulletPool[i].active) {
return this.bulletPool[i];
}
}
return null;
},
getMine: function getMine(isPlayerMine) {
var pool = isPlayerMine ? this.playerMinePool : this.aiMinePool;
for (var i = 0; i < pool.length; i++) {
if (!pool[i].active) {
return pool[i];
}
}
return null;
},
returnChip: function returnChip(chip) {
// Force-set inactive state
chip.active = false;
chip.visible = false;
chip.health = 0; // Ensure health is 0
// Reset tint on all visual components of the chip.
// This is to ensure that chip graphics don't retain the red damage flash tint
// when being reused from the pool.
if (chip.children) {
for (var i = 0; i < chip.children.length; i++) {
var child = chip.children[i];
if (child.tint !== undefined) {
child.tint = 0xffffff;
}
}
}
// NEW: Clear burn status completely
chip.burnDamage = 0;
chip.burnDuration = 0;
chip.burnTickTimer = 0;
// Clear ownership flag
var wasPlayerSide = chip.isPlayerSide;
chip.isPlayerSide = null;
// Remove from active arrays (check both arrays to be safe)
var playerIndex = activePlayerChips.indexOf(chip);
if (playerIndex !== -1) {
activePlayerChips.splice(playerIndex, 1);
}
var aiIndex = activeAIChips.indexOf(chip);
if (aiIndex !== -1) {
activeAIChips.splice(aiIndex, 1);
}
// Remove from containers
if (chip.parent) {
chip.parent.removeChild(chip);
}
// Double-check removal from both containers
if (activePlayerChipsContainer.children.indexOf(chip) !== -1) {
activePlayerChipsContainer.removeChild(chip);
}
if (activeAIChipsContainer.children.indexOf(chip) !== -1) {
activeAIChipsContainer.removeChild(chip);
}
},
returnBullet: function returnBullet(bullet) {
var explosionColor = 0x333333; // Dark Grey for clubs/spades
if (bullet.suit === 'hearts' || bullet.suit === 'diamonds') {
explosionColor = 0xff0000; // Red for hearts/diamonds
}
createExplosion(bullet.x, bullet.y, explosionColor);
bullet.active = false;
bullet.visible = false;
bullet.target = null; // Add this line to clear the target reference
var index = activeBullets.indexOf(bullet);
if (index !== -1) {
activeBullets.splice(index, 1);
}
gameLayer.removeChild(bullet);
},
returnMine: function returnMine(mine) {
mine.active = false;
mine.visible = false;
var index = activeMines.indexOf(mine);
if (index !== -1) {
activeMines.splice(index, 1);
}
if (mine.parent) {
mine.parent.removeChild(mine);
}
}
};
/****
* Path System
****/
var PathSystem = {
playerPath: [],
aiPath: [],
init: function init() {
// Create player path - rectangular loop around the play area
var padding = 130; // Distance from play area - increased by 50 pixels
var verticalPadding = padding - 25 - 30; // Reduced by 25 + 30 to decrease side length by 110px total
var leftX = PLAYER_AREA_X - padding + 10;
var rightX = PLAYER_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH + padding - 20 - 10;
var topY = PLAYER_AREA_Y - verticalPadding;
var bottomY = PLAYER_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT + verticalPadding;
this.playerPath = [
// Start at bottom left
{
x: leftX,
y: bottomY
},
// Go up the left side
{
x: leftX,
y: topY
},
// Go across the top
{
x: rightX,
y: topY
},
// Go down the right side
{
x: rightX,
y: bottomY
}];
// Create AI path - UPSIDE DOWN mirror of player path
var aiLeftX = AI_AREA_X - padding;
var aiRightX = AI_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH + padding;
var aiVerticalPadding = verticalPadding - 40 + 15; // Additional 40px reduction on each side for AI (80px total), extended by 15px
var aiTopY = AI_AREA_Y - aiVerticalPadding;
var aiBottomY = AI_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT + aiVerticalPadding;
this.aiPath = [
// Start at TOP left (opposite of player)
{
x: aiLeftX,
y: aiTopY
},
// Go DOWN the left side (opposite of player)
{
x: aiLeftX,
y: aiBottomY
},
// Go across the BOTTOM (opposite of player)
{
x: aiRightX,
y: aiBottomY
},
// Go UP the right side (opposite of player)
{
x: aiRightX,
y: aiTopY
}];
if (gameMode === 'coop') {
var centerX = SCREEN_WIDTH / 2;
var centerY = SCREEN_HEIGHT / 2;
var turningPointY = centerY - 200; // 200 pixels up from center
var goalX = SCREEN_WIDTH + 200;
this.playerPath = [{
x: leftX,
y: bottomY
}, {
x: leftX,
y: turningPointY
}, {
x: goalX,
y: turningPointY
}];
this.aiPath = [{
x: leftX,
y: aiTopY
}, {
x: leftX,
y: turningPointY
}, {
x: goalX,
y: turningPointY
}];
}
},
// Alternative offset method that spreads chips along the path direction:
getPathStart: function getPathStart(isPlayerSide) {
var baseStart = isPlayerSide ? this.playerPath[0] : this.aiPath[0];
var pathDirection = isPlayerSide ? this.playerPath[1] : this.aiPath[1];
// Calculate direction vector from start to next point
var dx = pathDirection.x - baseStart.x;
var dy = pathDirection.y - baseStart.y;
var length = Math.sqrt(dx * dx + dy * dy);
// Normalize direction vector
var normalizedX = dx / length;
var normalizedY = dy / length;
// Random offset along the path (backward from start point)
var pathOffset = Math.random() * 80; // 0-80 pixels back along path
var sideOffset = (Math.random() - 0.5) * 40; // -20 to +20 pixels to the side
return {
x: baseStart.x - normalizedX * pathOffset + normalizedY * sideOffset,
y: baseStart.y - normalizedY * pathOffset - normalizedX * sideOffset
};
},
getPositionAlongPath: function getPositionAlongPath(progress, isPlayerSide) {
var path = isPlayerSide ? this.playerPath : this.aiPath;
var pathLength = this.calculatePathLength(path);
var targetDistance = progress / 100 * pathLength;
if (targetDistance >= pathLength) {
return {
x: path[path.length - 1].x,
y: path[path.length - 1].y,
completed: true
};
}
var currentDistance = 0;
for (var i = 0; i < path.length - 1; i++) {
var segmentLength = this.getDistance(path[i], path[i + 1]);
if (currentDistance + segmentLength >= targetDistance) {
var segmentProgress = (targetDistance - currentDistance) / segmentLength;
return {
x: path[i].x + (path[i + 1].x - path[i].x) * segmentProgress,
y: path[i].y + (path[i + 1].y - path[i].y) * segmentProgress,
completed: false
};
}
currentDistance += segmentLength;
}
return {
x: path[path.length - 1].x,
y: path[path.length - 1].y,
completed: true
};
},
calculatePathLength: function calculatePathLength(path) {
var total = 0;
for (var i = 0; i < path.length - 1; i++) {
total += this.getDistance(path[i], path[i + 1]);
}
return total;
},
getDistance: function getDistance(p1, p2) {
var dx = p2.x - p1.x;
var dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
};
/****
* Chip Spawner
****/
var ChipSpawner = {
spawnChip: function spawnChip(value, isPlayerSide) {
var chip = PoolManager.getChip();
if (!chip) {
return;
}
var activeChips = isPlayerSide ? activePlayerChips : activeAIChips;
var minDistance = 85; // Half the chip diameter (170/2) to allow controlled overlap
var minDistanceSq = minDistance * minDistance;
var newPos;
// If no other chips exist, we can place it anywhere.
if (activeChips.length === 0) {
newPos = PathSystem.getPathStart(isPlayerSide);
} else {
var maxAttempts = 25; // Try to find a free spot with random placement first. It's fast.
var foundPosition = false;
for (var attempt = 0; attempt < maxAttempts; attempt++) {
var candidatePos = PathSystem.getPathStart(isPlayerSide);
var isValid = true;
for (var i = 0; i < activeChips.length; i++) {
var otherChip = activeChips[i];
var dx = candidatePos.x - otherChip.x;
var dy = candidatePos.y - otherChip.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < minDistanceSq) {
isValid = false;
break;
}
}
if (isValid) {
newPos = candidatePos;
foundPosition = true;
break;
}
}
// If random placements failed, use a more deterministic spiral search.
if (!foundPosition) {
var baseStart = PathSystem.getPathStart(isPlayerSide);
var searchRadius = minDistance; // Start searching just outside the minimum distance.
var angleStep = Math.PI / 6; // Check 12 directions.
var spiralAttempts = 30;
for (var i = 0; i < spiralAttempts; i++) {
for (var angle = 0; angle < Math.PI * 2; angle += angleStep) {
var testX = baseStart.x + Math.cos(angle) * searchRadius;
var testY = baseStart.y + Math.sin(angle) * searchRadius;
var isCandidateValid = true;
for (var j = 0; j < activeChips.length; j++) {
var otherChip = activeChips[j];
var dx = testX - otherChip.x;
var dy = testY - otherChip.y;
var distanceSq = dx * dx + dy * dy;
if (distanceSq < minDistanceSq) {
isCandidateValid = false;
break;
}
}
if (isCandidateValid) {
newPos = {
x: testX,
y: testY
};
foundPosition = true;
break;
}
}
if (foundPosition) {
break;
}
searchRadius += 20; // Spiral outwards.
}
}
// Ultimate fallback if no position is found (should be very rare).
if (!foundPosition) {
newPos = PathSystem.getPathStart(isPlayerSide);
}
}
chip.activate(value, isPlayerSide, newPos);
// IMPORTANT: Add chip to active array BEFORE the container
// This ensures that subsequent chips spawned in the same batch
// will detect this chip when checking for overlaps
if (isPlayerSide) {
activePlayerChips.push(chip);
activePlayerChipsContainer.addChildAt(chip, 0);
} else {
activeAIChips.push(chip);
activeAIChipsContainer.addChildAt(chip, 0);
}
},
spawnChipAtPosition: function spawnChipAtPosition(value, isPlayerSide, position) {
var chip = PoolManager.getChip();
if (!chip) {
return;
}
chip.activate(value, isPlayerSide, position);
// Add chip to active array and container
if (isPlayerSide) {
activePlayerChips.push(chip);
activePlayerChipsContainer.addChildAt(chip, 0);
} else {
activeAIChips.push(chip);
activeAIChipsContainer.addChildAt(chip, 0);
}
}
};
/****
* Mod System
****/
var ModSystem = {
equippedMods: {
hearts: null,
diamonds: null,
clubs: null,
spades: null
},
aiEquippedMods: {
hearts: null,
diamonds: null,
clubs: null,
spades: null
},
modData: {
burnHeartMod: {
suit: 'hearts',
name: 'Flame',
description: 'Hearts bullets apply burning damage over time. Burn damage is 10% of hit damage per tick for 4 seconds. Burn effects stack.'
},
unityHeartsMod: {
suit: 'hearts',
name: 'Unity',
description: 'Increases bullet damage by 25% for each adjacent Hearts card.'
},
chipsDiamondMod: {
suit: 'diamonds',
name: 'Greed',
description: 'Earn bonus chips when enemies are defeated, based on diamonds on board. Bonus scales with card level.'
},
gamblerDiamondsMod: {
suit: 'diamonds',
name: 'Gambler',
description: 'Diamonds have a chance to deal double damage or none at all. The chance for double damage increases with card level.'
},
boostDiamondsMod: {
suit: 'diamonds',
name: 'Boost',
description: 'Diamonds boost the attack power of adjacent cards. The boost increases with the card\'s level.'
},
freezeSpadeMod: {
suit: 'spades',
name: 'Freeze',
description: 'Spades have a chance to freeze enemies in place when dealing damage. Duration scales with card level.'
},
spreadClubMod: {
suit: 'clubs',
name: 'Spreadshot',
description: 'Clubs deal reduced damage but hit multiple enemies. Extra targets scale with card level.'
},
slowClubsMod: {
suit: 'clubs',
name: 'Slow',
description: 'Clubs bullets slow enemies on hit. Slow amount and duration scale with card level.'
},
ricochetClubsMod: {
suit: 'clubs',
name: 'Ricochet',
description: 'Clubs bullets ricochet amongst enemies, with limited bounces, dealing less damage with each one.'
},
mineSpadesMod: {
suit: 'spades',
name: 'Mine',
description: 'Spades cards place mines on the enemy path that explode on contact, dealing area damage.'
},
deathSpadesMod: {
suit: 'spades',
name: 'Execute',
description: 'When equipped spades bullets have a chance to execute low health targets immediately.'
},
investmentHeartsMod: {
suit: 'hearts',
name: 'Investment',
description: 'Hearts cards generate income while on board. Income increases the longer the card is on board and resets on level up.'
}
},
currentlyDisplayedMod: null,
modDisplayContainer: null,
topSuitGraphics: [],
ownedMods: {},
// Store references to top suit graphics
init: function init() {
if (storage.equippedMods) {
this.equippedMods = storage.equippedMods;
}
if (storage.ownedMods) {
this.ownedMods = storage.ownedMods;
}
},
saveToStorage: function saveToStorage() {
storage.equippedMods = this.equippedMods;
storage.ownedMods = this.ownedMods;
},
isModOwned: function isModOwned(modAssetId) {
return !!this.ownedMods[modAssetId];
},
unlockMod: function unlockMod(modAssetId) {
this.ownedMods[modAssetId] = true;
this.saveToStorage();
},
getUnownedMods: function getUnownedMods() {
var unowned = [];
for (var modId in this.modData) {
if (!this.isModOwned(modId)) {
unowned.push(modId);
}
}
return unowned;
},
equipMod: function equipMod(modAssetId) {
var modData = this.modData[modAssetId];
if (!modData) {
return;
}
this.equippedMods[modData.suit] = modAssetId;
this.saveToStorage();
this.updateTopSuitDisplay();
this.hideModDisplay();
},
updateTopSuitDisplay: function updateTopSuitDisplay() {
var suitAssets = ['heartSuit', 'diamondSuit', 'clubSuit', 'spadeSuit'];
var suits = ['hearts', 'diamonds', 'clubs', 'spades'];
for (var i = 0; i < this.topSuitGraphics.length; i++) {
var container = this.topSuitGraphics[i];
if (container) {
// Make it an Ace card if it's not already
var hasAceText = false;
for (var k = 0; k < container.children.length; k++) {
if (container.children[k].isAceText) {
hasAceText = true;
break;
}
}
if (!hasAceText) {
var suitForAce = suits[i];
var suitColor = CardSystem.suitColors[suitForAce] || 0x000000;
var valueText = new Text2('A', {
size: 56 * 1.5,
fill: suitColor,
weight: 800,
stroke: 0x000000,
strokeThickness: 0
});
valueText.isAceText = true;
valueText.anchor.set(0, 0);
// The card background is scaled 1.5x, these coords are scaled to match.
valueText.x = -95 * 1.5;
valueText.y = -135 * 1.5;
container.addChild(valueText);
}
// Remove the previous mod/suit icon if it exists
for (var j = container.children.length - 1; j >= 0; j--) {
if (container.children[j].isSuitIcon) {
container.removeChildAt(j);
break;
}
}
// Add the appropriate icon (either mod or default suit)
var suit = suits[i];
var equippedMod = this.equippedMods[suit];
var assetId = equippedMod || suitAssets[i];
var suitIcon = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
suitIcon.isSuitIcon = true;
suitIcon.scale.set(1.2);
// Center it in the background card
suitIcon.x = 0;
suitIcon.y = 0;
container.addChild(suitIcon);
}
}
},
showModDisplay: function showModDisplay(modAssetId, sourceX, sourceY) {
if (this.currentlyDisplayedMod) {
return;
}
var modData = this.modData[modAssetId];
if (!modData) {
return;
}
this.currentlyDisplayedMod = modAssetId;
this.modDisplayContainer = new Container();
this.modDisplayContainer.interactive = true;
uiLayer.addChild(this.modDisplayContainer);
// Add a semi-transparent background to catch clicks outside the popup
var bgBlocker = new Container();
bgBlocker.interactive = true;
bgBlocker.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
this.modDisplayContainer.addChild(bgBlocker);
var enlargedMod = LK.getAsset(modAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
enlargedMod.x = SCREEN_WIDTH / 2;
enlargedMod.y = SCREEN_HEIGHT / 2 - 450;
enlargedMod.scale.set(1);
this.modDisplayContainer.addChild(enlargedMod);
var overlayWidth = 600;
var overlayHeight = 400;
var descriptionOverlay = new Container();
var overlayBg = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
overlayBg.scale.set(overlayWidth / 150, overlayHeight / 240);
overlayBg.tint = 0x000000;
overlayBg.alpha = 0.8;
descriptionOverlay.addChild(overlayBg);
var descText = new Text2(modData.description, {
size: 50,
fill: 0xffffff,
weight: 600,
stroke: 0x000000,
strokeThickness: 2,
align: 'center',
wordWrap: true,
wordWrapWidth: overlayWidth - 40
});
descText.anchor.set(0.5, 0);
descText.y = -overlayHeight / 2 + 40;
descriptionOverlay.addChild(descText);
var equipButton = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
equipButton.scale.set(1.5, 0.5);
equipButton.tint = 0x000000;
equipButton.alpha = 0.8;
equipButton.y = overlayHeight / 2 + 150;
descriptionOverlay.addChild(equipButton);
var equipText = new Text2('Equip', {
size: 50,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 3
});
equipText.anchor.set(0.5, 0.5);
equipText.y = equipButton.y;
descriptionOverlay.addChild(equipText);
descriptionOverlay.x = SCREEN_WIDTH / 2;
descriptionOverlay.y = SCREEN_HEIGHT / 2 + 250;
descriptionOverlay.alpha = 0;
this.modDisplayContainer.addChild(descriptionOverlay);
var self = this;
var isOwned = this.isModOwned(modAssetId);
if (isOwned) {
equipText.setText('Equip');
equipButton.down = function () {
self.equipMod(modAssetId);
};
} else {
equipText.setText('Not Owned');
equipButton.interactive = false;
equipButton.tint = 0x333333;
enlargedMod.tint = 0x808080;
}
tween(enlargedMod, {
scaleX: 4,
scaleY: 4
}, {
duration: 300,
easing: tween.backOut
});
tween(descriptionOverlay, {
alpha: 1
}, {
duration: 200,
delay: 150
});
bgBlocker.down = function () {
self.hideModDisplay();
};
},
hideModDisplay: function hideModDisplay() {
if (!this.modDisplayContainer) {
return;
}
var self = this;
tween(this.modDisplayContainer, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
if (self.modDisplayContainer && self.modDisplayContainer.parent) {
self.modDisplayContainer.parent.removeChild(self.modDisplayContainer);
}
self.modDisplayContainer = null;
self.currentlyDisplayedMod = null;
}
});
},
getEquippedModAsset: function getEquippedModAsset(suit, isPlayerCard) {
if (isPlayerCard === undefined) {
isPlayerCard = true;
}
var mods = isPlayerCard ? this.equippedMods : this.aiEquippedMods;
return mods[suit] || null;
}
};
/****
* Wave Spawning System
****/
var WaveSystem = {
playerSpawnTimer: 0,
aiSpawnTimer: 0,
waveNumber: 1,
waveTimer: 0,
waveDuration: 1800,
// 30 seconds per wave (30 * 60 ticks)
spawnInterval: 45,
// Much faster: spawn every 0.75 seconds instead of 2 seconds
bossSpawned: false,
// Track if boss has been spawned this wave
getChipValue: function getChipValue(waveNumber) {
// Wave 1: Only 1-chips (easier)
if (waveNumber === 1) {
return 1;
}
// Wave 2-3: More 5s introduced earlier for increased difficulty
else if (waveNumber <= 3) {
return Math.random() < 0.2 ? 5 : 1; // 20% chance of 5-chip (increased from 10%)
}
// Wave 4-6: Even more 5s mixed in
else if (waveNumber <= 6) {
return Math.random() < 0.35 ? 5 : 1; // 35% chance of 5-chip (increased from 25%)
}
// Wave 7-9: More 5s than 1s for increased difficulty
else if (waveNumber <= 9) {
return Math.random() < 0.65 ? 5 : 1; // 65% chance of 5-chip (increased from 50%)
}
// Wave 11-15: Introduce 10-chips earlier and more frequently
else if (waveNumber <= 15) {
var rand = Math.random();
if (rand < 0.15) {
return 10;
} else if (rand < 0.7) {
return 5;
} else {
return 1;
}
}
// Wave 16-19: More variety with higher values
else if (waveNumber <= 19) {
var rand = Math.random();
if (rand < 0.3) {
return 10;
} else if (rand < 0.8) {
return 5;
} else {
return 1;
}
}
// Wave 21+: Keep scaling gradually with higher difficulty
else if (!this.isBossWave(waveNumber)) {
var rand = Math.random();
if (rand < 0.08) {
return 25;
} else if (rand < 0.4) {
return 10;
} else if (rand < 0.85) {
return 5;
} else {
return 1;
}
}
},
getBossChipValue: function getBossChipValue(waveNumber) {
// Mini-boss waves (waves 5, 15, 25, etc.)
if (waveNumber % 10 === 5) {
var miniBossLevel = Math.floor(waveNumber / 10);
var baseMiniBossValue = 100; // Purple chip value
return baseMiniBossValue * Math.pow(2, miniBossLevel);
}
// Main boss waves (waves 10, 20, 30, etc.)
var bossLevel = Math.floor(waveNumber / 10);
// Base value derived from total health of wave 9 enemies
var baseBossValue = 210;
return baseBossValue * Math.pow(2, bossLevel - 1);
},
isBossWave: function isBossWave(waveNumber) {
// Both PvP and Coop now have mini-bosses every 5 waves and main bosses every 10 waves
return waveNumber > 0 && (waveNumber % 10 === 0 || waveNumber % 10 === 5);
},
spawnChip: function spawnChip(isPlayerSide) {
if (this.isBossWave(this.waveNumber)) {
// Boss wave: spawn ONE big enemy at the start, then nothing
if (!this.bossSpawned) {
var bossValue = this.getBossChipValue(this.waveNumber);
ChipSpawner.spawnChip(bossValue, isPlayerSide);
this.bossSpawned = true;
console.log("BOSS spawned with value:", bossValue);
}
// Don't spawn anything else during boss wave
return;
}
// Normal wave spawning - always single enemy
var chipValue = this.getChipValue(this.waveNumber);
ChipSpawner.spawnChip(chipValue, isPlayerSide);
},
playerBossDefeated: false,
aiBossDefeated: false,
update: function update() {
this.waveTimer++;
// Check if wave is complete
if (this.waveTimer >= this.waveDuration) {
// If it's a boss wave, only advance if both bosses are defeated.
if (this.isBossWave(this.waveNumber) && (!this.playerBossDefeated || !this.aiBossDefeated)) {
// Don't end the wave, just let it continue until bosses are defeated.
} else {
// For normal waves, or for boss waves where both bosses are defeated.
// Check if both bosses were defeated in a boss wave
if (this.isBossWave(this.waveNumber) && this.playerBossDefeated && this.aiBossDefeated) {
var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS";
console.log("Both " + bossType + "es defeated! Moving to next round!");
createFloatingText('ROUND COMPLETE!', SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0x00ff00);
// Reset boss defeat flags
this.playerBossDefeated = false;
this.aiBossDefeated = false;
}
this.waveTimer = 0;
this.waveNumber++;
this.playerSpawnTimer = 0;
this.aiSpawnTimer = 0;
this.bossSpawned = false; // Reset boss spawn flag
var waveType;
if (this.isBossWave(this.waveNumber)) {
waveType = this.waveNumber % 10 === 5 ? "MINI-BOSS WAVE" : "BOSS WAVE";
} else {
waveType = "Wave";
}
console.log(waveType + " " + this.waveNumber + " starting!");
return;
}
}
// BOSS WAVE LOGIC - Spawn bosses immediately at start of wave
if (this.isBossWave(this.waveNumber)) {
if (!this.bossSpawned && this.waveTimer === 1) {
// Only on first tick of boss wave
var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS";
console.log("=== " + bossType + " WAVE " + this.waveNumber + " STARTING ===");
// Spawn player boss
var playerBossValue = this.getBossChipValue(this.waveNumber);
ChipSpawner.spawnChip(playerBossValue, true);
console.log("PLAYER " + bossType + " spawned with value:", playerBossValue, "Active player chips:", activePlayerChips.length);
// Spawn AI boss (only in PvP mode - coop doesn't have AI bosses)
if (gameMode === 'pvp') {
var aiBossValue = this.getBossChipValue(this.waveNumber);
ChipSpawner.spawnChip(aiBossValue, false);
console.log("AI " + bossType + " spawned with value:", aiBossValue, "Active AI chips:", activeAIChips.length);
} else {
// In coop, AI side is automatically "defeated" since there are no AI enemies
this.aiBossDefeated = true;
}
this.bossSpawned = true;
}
// Boss defeat detection - only after bosses have had time to spawn
if (this.bossSpawned && this.waveTimer > 120) {
// 2 second delay
var bossType = this.waveNumber % 10 === 5 ? "MINI-BOSS" : "BOSS";
console.log("Checking " + bossType.toLowerCase() + " status - Player chips:", activePlayerChips.length, "AI chips:", activeAIChips.length);
// Check if player side boss is defeated
if (!this.playerBossDefeated && activePlayerChips.length === 0) {
this.playerBossDefeated = true;
console.log("Player defeated the " + bossType.toLowerCase() + "!");
createFloatingText(bossType + ' DEFEATED!', SCREEN_WIDTH / 2, PLAYER_AREA_Y + SLOT_HEIGHT, 0xffd700);
}
// Check if AI side boss is defeated (only in PvP)
if (gameMode === 'pvp' && !this.aiBossDefeated && activeAIChips.length === 0) {
this.aiBossDefeated = true;
console.log("AI defeated the " + bossType.toLowerCase() + "!");
createFloatingText('AI ' + bossType + ' DEFEATED!', SCREEN_WIDTH / 2, AI_AREA_Y + SLOT_HEIGHT, 0xffd700);
}
// If both bosses defeated, immediately end the wave
if (this.playerBossDefeated && this.aiBossDefeated) {
this.waveTimer = this.waveDuration - 1; // Set to end on next tick
}
}
return; // Don't do normal spawning during boss waves
}
// NORMAL WAVE LOGIC
var currentSpawnInterval = this.spawnInterval;
if (this.waveNumber === 1) {
currentSpawnInterval = 75; // Slightly faster spawning for wave 1 (1.25 seconds)
} else if (this.waveNumber >= 2) {
currentSpawnInterval = Math.max(45, 60 - Math.floor((this.waveNumber - 2) * 15)); // Wave 2 is slower (60), then speeds up to normal (45) for wave 3+
}
// Spawn on player side
this.playerSpawnTimer++;
if (this.playerSpawnTimer >= currentSpawnInterval) {
this.playerSpawnTimer = 0;
this.spawnChip(true);
}
// Spawn on AI side
this.aiSpawnTimer++;
if (this.aiSpawnTimer >= currentSpawnInterval) {
this.aiSpawnTimer = 0;
this.spawnChip(false);
}
}
};
/****
* Game State
****/
var gameState = {
playerChips: 200,
aiChips: 200,
playerLives: 3,
aiLives: 3,
isPlayerTurn: true,
dealCost: 25,
// Lowered from 38 to 25
// Starting cost
dealCount: 0,
// ... rest remains the same
playerDeck: [],
playerHand: [],
playerPlayArea: [],
aiDeck: [],
aiPlayArea: []
};
// Game state management
var currentGameState = 'start'; // 'start' or 'playing'
var startScreenElements = [];
var gameElements = [];
/****
* Game Variables
****/
var activePlayerChips = [];
var activeAIChips = [];
var activeBullets = [];
var activeMines = [];
var playerTotalDamage = 0;
var aiTotalDamage = 0;
var playerHandNameTexts = [];
var backgroundSuits = [];
var slotIndicators = [];
var selectedCard = null;
var isDragging = false;
var originalCardPosition = null;
var gameReady = false;
var activePlayerChipsContainer = new Container(); // Container for player chips
var activeAIChipsContainer = new Container(); // Container for AI chips
var playerLifeHearts = [];
var aiLifeHearts = [];
var opponentNameText = null;
var playerNameText = null;
var lastPlayerLives = 0;
var lastAiLives = 0;
var gameMode = 'pvp';
var gameLayer = new Container();
var floorBackground = LK.getAsset('floorbackround', {
anchorX: 0.5,
anchorY: 0.5
});
floorBackground.x = SCREEN_WIDTH / 2;
floorBackground.y = SCREEN_HEIGHT / 2;
gameLayer.addChild(floorBackground);
var uiLayer = new Container();
game.addChild(gameLayer);
game.addChild(uiLayer);
/****
* UI Elements
****/
var playerChipsText = new Text2('Chips: 200', {
size: 50,
fill: 0xffd700,
weight: 800,
stroke: 0x000000,
strokeThickness: 0
});
playerChipsText.x = 50;
playerChipsText.y = SCREEN_HEIGHT - 120;
playerChipsText.visible = false;
uiLayer.addChild(playerChipsText);
gameElements.push(playerChipsText);
// Add AI stats too for clarity
var waveText = new Text2('Wave: 1', {
size: 40,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 0
});
waveText.x = SCREEN_WIDTH - 200;
waveText.y = 50;
waveText.visible = false;
uiLayer.addChild(waveText);
gameElements.push(waveText);
var discardAreaContainer = new Container();
var dealButtonGraphic = discardAreaContainer.attachAsset('dealButton', {
anchorX: 0.5,
anchorY: 0.5
});
var refundButtonGraphic = discardAreaContainer.attachAsset('refundButton', {
anchorX: 0.5,
anchorY: 0.5
});
refundButtonGraphic.visible = false;
var discardAreaGraphic = dealButtonGraphic;
var discardText = new Text2('-25', {
size: 50,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 0
});
discardText.anchor.set(0.5, 0.5);
discardText.y = 110;
discardAreaContainer.addChild(discardText);
// Position it right of the hand
var handWidthForDiscard = 5 * DEAL_SLOT_WIDTH + 4 * 30;
var handStartXForDiscard = (SCREEN_WIDTH - handWidthForDiscard) / 2;
var discardX = handStartXForDiscard + handWidthForDiscard + 30 + DEAL_SLOT_WIDTH / 2;
// Make sure it doesn't go off screen
if (discardX + DEAL_SLOT_WIDTH / 2 > SCREEN_WIDTH) {
discardX = SCREEN_WIDTH - DEAL_SLOT_WIDTH / 2 - 20;
}
discardAreaContainer.x = discardX + 10 + 10;
discardAreaContainer.y = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2 + 10 - 10;
discardAreaContainer.visible = false;
uiLayer.addChild(discardAreaContainer);
gameElements.push(discardAreaContainer);
discardAreaContainer.down = function () {
if (!isDragging && gameState.playerChips >= gameState.dealCost) {
dealNewHand();
}
};
/****
* Game Functions
****/
// --- Kill counter for extra enemy spawn (player kills only) ---
var playerKillCounter = 0;
function handleChipKill(isPlayerSide) {
// Only count chip kills for extra enemy spawn in PvP mode
if (gameMode !== 'pvp') {
return;
}
// Only track player kills and spawn extra chips for the AI
if (isPlayerSide) {
playerKillCounter++;
if (playerKillCounter >= 5) {
playerKillCounter = 0;
// Spawn extra enemy on AI side
var chipValue = WaveSystem.getChipValue(WaveSystem.waveNumber);
ChipSpawner.spawnChip(chipValue, false);
}
}
}
function dealInitialHand() {
var cardsToDeal = 5;
var dealDelay = 150; // ms
var dealCard = function dealCard(i) {
if (gameState.playerDeck.length === 0) {
gameState.playerDeck = CardSystem.createDeck();
}
var cardData = gameState.playerDeck.pop();
var startLevel = Math.floor((WaveSystem.waveNumber - 1) / 10) + 1;
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30;
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
var slotX = handStartX + i * DEAL_SLOT_WIDTH + i * 30 + DEAL_SLOT_WIDTH / 2;
var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2;
var startX = slotX;
var startY = SCREEN_HEIGHT + DEAL_SLOT_HEIGHT;
// Mark slot as occupied immediately to prevent overwriting if deal is pressed too quickly
gameState.playerHand[i] = true;
var cardBack = LK.getAsset('cardBack', {
anchorX: 0.5,
anchorY: 0.5
});
cardBack.x = startX;
cardBack.y = startY;
uiLayer.addChild(cardBack);
// Animate card flying into place and flipping
tween(cardBack, {
x: slotX,
y: slotY
}, {
duration: 250,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Flip animation: shrink back
tween(cardBack, {
scaleX: 0.01
}, {
duration: 150,
easing: tween.quadIn,
onFinish: function onFinish() {
if (cardBack.parent) {
cardBack.parent.removeChild(cardBack);
}
// Create and show the real card
var card = new Card(cardData);
card.setLevel(startLevel);
card.activate(slotX, slotY, false, true);
card.scale.x = 0.01;
uiLayer.addChild(card);
// Replace the placeholder with the actual card object
gameState.playerHand[i] = card;
// Flip animation: expand card face
tween(card, {
scaleX: 1.0
}, {
duration: 150,
easing: tween.quadOut,
onFinish: function onFinish() {
LK.getSound('cardLand').play();
}
});
}
});
}
});
};
for (var i = 0; i < cardsToDeal; i++) {
(function (index) {
LK.setTimeout(function () {
dealCard(index);
}, index * dealDelay);
})(i);
}
// Enable deal button after all animations are scheduled to finish
var totalAnimationTime = (cardsToDeal - 1) * dealDelay + 250 + 150 + 200; // Added buffer
LK.setTimeout(function () {
if (discardAreaContainer) {
discardAreaContainer.interactive = true;
updateUI();
}
}, totalAnimationTime);
}
function createBurnEffect(x, y) {
var numParticles = 4; // Slightly more particles
for (var i = 0; i < numParticles; i++) {
var particle = LK.getAsset('burnHeartMod', {
anchorX: 0.5,
anchorY: 0.5
});
particle.x = x + (Math.random() - 0.5) * 80; // Wider spread
particle.y = y + (Math.random() - 0.5) * 80;
particle.scale.set(0.3 + Math.random() * 0.2); // Adjusted for new asset
var angle = Math.random() * Math.PI * 2;
var distance = Math.random() * 40 + 25; // Larger movement distance
var duration = 400 + Math.random() * 500; // Slightly longer duration
var targetX = x + Math.cos(angle) * distance;
var targetY = y + Math.sin(angle) * distance - Math.random() * 50; // Float upward more
gameLayer.addChild(particle);
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: duration,
easing: tween.quadOut,
onFinish: function (p) {
return function () {
if (p.parent) {
p.parent.removeChild(p);
}
};
}(particle)
});
}
}
function createIceSparkles(x, y) {
var numSparkles = 2;
for (var i = 0; i < numSparkles; i++) {
var sparkle = LK.getAsset('iceCube', {
anchorX: 0.5,
anchorY: 0.5
});
sparkle.x = x + (Math.random() - 0.5) * 60;
sparkle.y = y + (Math.random() - 0.5) * 60;
sparkle.scale.set(0.1 + Math.random() * 0.1);
sparkle.alpha = 0.6;
sparkle.tint = 0x88ddff; // Light blue tint
gameLayer.addChild(sparkle);
tween(sparkle, {
y: sparkle.y - 20,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 800,
easing: tween.quadOut,
onFinish: function (p) {
return function () {
if (p.parent) {
p.parent.removeChild(p);
}
};
}(sparkle)
});
}
}
function createIceShatterEffect(x, y) {
var numShards = 6;
for (var i = 0; i < numShards; i++) {
var shard = LK.getAsset('iceCube', {
anchorX: 0.5,
anchorY: 0.5
});
shard.x = x;
shard.y = y;
shard.scale.set(0.2 + Math.random() * 0.2);
shard.tint = 0xaaeeff; // Ice blue tint
var angle = Math.PI * 2 * i / numShards + (Math.random() - 0.5) * 0.5;
var distance = 40 + Math.random() * 30;
var targetX = x + Math.cos(angle) * distance;
var targetY = y + Math.sin(angle) * distance;
gameLayer.addChild(shard);
tween(shard, {
x: targetX,
y: targetY,
alpha: 0,
rotation: Math.random() * Math.PI * 2,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 600,
easing: tween.quadOut,
onFinish: function (p) {
return function () {
if (p.parent) {
p.parent.removeChild(p);
}
};
}(shard)
});
}
}
function createModFloatAnimation(target, shadow) {
var initialY = target.y;
var floatDistance = 10;
var baseDuration = 3000;
var randomDurationOffset = 1500;
var initialScale = shadow.scale.x;
var minScale = initialScale * 0.7;
var maxScale = initialScale * 1.0;
function floatUp() {
if (!target.parent || !target.parent.parent || !target.parent.parent.parent) {
return;
}
var duration = baseDuration / 2 + Math.random() * randomDurationOffset;
tween(target, {
y: initialY - floatDistance
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: floatDown
});
tween(shadow, {
scaleX: minScale,
scaleY: minScale
}, {
duration: duration,
easing: tween.easeInOut
});
}
function floatDown() {
if (!target.parent || !target.parent.parent || !target.parent.parent.parent) {
return;
}
var duration = baseDuration / 2 + Math.random() * randomDurationOffset;
tween(target, {
y: initialY + floatDistance
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: floatUp
});
tween(shadow, {
scaleX: maxScale,
scaleY: maxScale
}, {
duration: duration,
easing: tween.easeInOut
});
}
LK.setTimeout(floatDown, Math.random() * baseDuration);
}
// Add this new function to calculate greed bonus
function calculateGreedBonus(isPlayerSide, baseChipsEarned) {
// Only calculate bonus if greed mod is equipped
if (ModSystem.getEquippedModAsset('diamonds', isPlayerSide) !== 'chipsDiamondMod') {
return 0;
}
var playArea = isPlayerSide ? gameState.playerPlayArea : gameState.aiPlayArea;
var totalBonusPercentage = 0;
// Calculate bonus from each diamond card on the board
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = playArea[row][col];
if (card && card.cardData.suit === 'diamonds') {
// The "base gain" for this card is 5%
var baseGain = 0.05;
// Each level adds an additional 10% gain
var levelGain = card.level * 0.1;
totalBonusPercentage += baseGain + levelGain;
}
}
}
// Do not return fractional gains - round up
return Math.ceil(baseChipsEarned * totalBonusPercentage);
}
function createStartScreen(fromMatch) {
LK.playMusic('titleSong');
ModSystem.init();
// Clear any existing start screen elements
startScreenElements.forEach(function (element) {
if (element.parent) {
element.parent.removeChild(element);
}
});
startScreenElements = [];
var battleScreenContainer;
// Add background animation
var startScreenAnimContainer = new Container();
uiLayer.addChild(startScreenAnimContainer);
startScreenElements.push(startScreenAnimContainer);
var suitAssets = ['heartSuit', 'diamondSuit', 'clubSuit', 'spadeSuit'];
var spacing = 400;
var numCols = Math.ceil(SCREEN_WIDTH / spacing) + 3;
var numRows = Math.ceil(SCREEN_HEIGHT / spacing) + 3;
var patternWidth = numCols * spacing;
var patternHeight = numRows * spacing;
backgroundSuits = [];
for (var row = 0; row < numRows; row++) {
for (var col = 0; col < numCols; col++) {
var suitIndex = (row + col) % suitAssets.length;
var suitId = suitAssets[suitIndex];
var suit = LK.getAsset(suitId, {
anchorX: 0.5,
anchorY: 0.5
});
suit.x = col * spacing - spacing;
suit.y = row * spacing - spacing;
suit.alpha = 0.8;
suit.scale.set(1.5);
suit.baseX = col * spacing;
suit.baseY = row * spacing;
startScreenAnimContainer.addChild(suit);
backgroundSuits.push(suit);
}
}
var gameLogo = LK.getAsset('titleLogo', {
anchorX: 0.5,
anchorY: 0.5
});
gameLogo.x = SCREEN_WIDTH / 2;
gameLogo.y = SCREEN_HEIGHT / 2 - 200;
uiLayer.addChild(gameLogo);
startScreenElements.push(gameLogo);
var bottomBarContainer = new Container();
uiLayer.addChild(bottomBarContainer);
startScreenElements.push(bottomBarContainer);
function showBattleSelection() {
gameLogo.visible = false;
bottomBarContainer.visible = true;
battleScreenContainer = new Container();
uiLayer.addChild(battleScreenContainer);
startScreenElements.push(battleScreenContainer);
var pvpIcon = LK.getAsset('pvpIcon', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
pvpIcon.scale.set(3);
pvpIcon.x = SCREEN_WIDTH * 0.25;
pvpIcon.y = SCREEN_HEIGHT * 0.45;
battleScreenContainer.addChild(pvpIcon);
var pvpButton = LK.getAsset('pvpButton', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
pvpButton.scale.set(3);
pvpButton.x = pvpIcon.x;
pvpButton.y = pvpIcon.y + pvpIcon.height / 2 + pvpButton.height / 2 + 150;
battleScreenContainer.addChild(pvpButton);
var battleRatingText = new Text2('Battle rating: ' + storage.battleRating, {
size: 70,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 4
});
battleRatingText.anchor.set(0.5, 0.5);
var battleRatingIcon = LK.getAsset('battleRatingIcon', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
var totalWidth = battleRatingIcon.width + 10 + battleRatingText.width;
var yPos = pvpIcon.y + pvpIcon.height / 2 + 350;
battleRatingIcon.x = pvpIcon.x - totalWidth / 2 + battleRatingIcon.width / 2;
battleRatingIcon.y = yPos;
battleRatingText.x = battleRatingIcon.x + battleRatingIcon.width / 2 + 10 + battleRatingText.width / 2;
battleRatingText.y = yPos;
battleScreenContainer.addChild(battleRatingIcon);
battleScreenContainer.addChild(battleRatingText);
var coopIcon = LK.getAsset('coopIcon', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
coopIcon.scale.set(3);
coopIcon.x = SCREEN_WIDTH * 0.75;
coopIcon.y = pvpIcon.y;
battleScreenContainer.addChild(coopIcon);
var coopButton = LK.getAsset('coopButton', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
coopButton.scale.set(3);
coopButton.x = coopIcon.x;
coopButton.y = coopIcon.y + coopIcon.height / 2 + coopButton.height / 2 + 150;
battleScreenContainer.addChild(coopButton);
var coopHighestWaveText = new Text2('Highest wave: ' + (storage.coopHighestWave || 0), {
size: 70,
fill: 0xffffff,
weight: 800,
stroke: 0x000000,
strokeThickness: 4
});
coopHighestWaveText.anchor.set(0.5, 0.5);
coopHighestWaveText.x = coopIcon.x;
coopHighestWaveText.y = coopIcon.y + coopIcon.height / 2 + 350;
battleScreenContainer.addChild(coopHighestWaveText);
// === DEBUG BUTTON REMOVED ===
function onPlay(isTutorial, mode) {
startGame(isTutorial, mode);
}
if (!storage.tutorialCompleted && fromMatch !== true) {
TutorialSystem.start();
var items = TutorialSystem.startScreenItems;
items.mods.button.interactive = false;
items.mods.icon.interactive = false;
items.shop.button.interactive = false;
items.shop.icon.interactive = false;
items.mods.button.alpha = 0.5;
items.mods.icon.alpha = 0.5;
items.shop.button.alpha = 0.5;
items.shop.icon.alpha = 0.5;
var overlay = new Container();
overlay.interactive = true;
overlay.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
var bg = LK.getAsset('card', {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
anchorX: 0,
anchorY: 0
});
bg.tint = 0x000000;
bg.alpha = 0.7;
overlay.addChild(bg);
battleScreenContainer.addChild(overlay);
battleScreenContainer.removeChild(pvpIcon);
battleScreenContainer.removeChild(pvpButton);
overlay.addChild(pvpIcon);
overlay.addChild(pvpButton);
coopIcon.interactive = false;
coopIcon.alpha = 0.3;
coopButton.interactive = false;
coopButton.alpha = 0.3;
TutorialSystem.pulseAnimation(pvpIcon);
TutorialSystem.pulseAnimation(pvpButton);
var welcomeText = new Text2("Welcome to Double Down Defense! Let’s jump right into a game, tap on PvP to start!", {
size: 70,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 5,
align: 'center',
wordWrap: true,
wordWrapWidth: SCREEN_WIDTH - 200
});
welcomeText.anchor.set(0.5, 0.5);
welcomeText.x = SCREEN_WIDTH / 2;
welcomeText.y = SCREEN_HEIGHT * 0.25;
overlay.addChild(welcomeText);
pvpIcon.down = function () {
onPlay(true, 'pvp');
};
pvpButton.down = function () {
onPlay(true, 'pvp');
};
} else {
pvpIcon.down = function () {
onPlay(false, 'pvp');
};
pvpButton.down = function () {
onPlay(false, 'pvp');
};
coopIcon.down = function () {
onPlay(false, 'coop');
};
coopButton.down = function () {
onPlay(false, 'coop');
};
}
}
if (fromMatch) {
showBattleSelection();
} else {
// Animate logo scaling in
gameLogo.scale.set(0.01);
bottomBarContainer.visible = false;
tween(gameLogo, {
scaleX: 1,
scaleY: 1
}, {
duration: 1200,
easing: tween.elasticOut,
onFinish: function onFinish() {
var tapToStartText = new Text2('Tap anywhere to start', {
size: 90,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 6
});
tapToStartText.anchor.set(0.5, 0.5);
tapToStartText.x = SCREEN_WIDTH / 2;
tapToStartText.y = gameLogo.y + gameLogo.height / 2 + 250;
uiLayer.addChild(tapToStartText);
startScreenElements.push(tapToStartText);
function flashAnimation() {
if (!tapToStartText.parent) {
return;
}
tween(tapToStartText, {
alpha: 0.2
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!tapToStartText.parent) {
return;
}
tween(tapToStartText, {
alpha: 1.0
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: flashAnimation
});
}
});
}
flashAnimation();
var screenTapArea = new Container();
screenTapArea.interactive = true;
screenTapArea.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
uiLayer.addChild(screenTapArea);
startScreenElements.push(screenTapArea);
screenTapArea.down = function () {
tween.stop(tapToStartText);
uiLayer.removeChild(tapToStartText);
uiLayer.removeChild(screenTapArea);
showBattleSelection();
};
}
});
}
var startBottomBar = LK.getAsset('bottomBar', {
anchorX: 0.5,
anchorY: 1
});
startBottomBar.x = SCREEN_WIDTH / 2;
startBottomBar.y = SCREEN_HEIGHT;
bottomBarContainer.addChild(startBottomBar);
var topBar = LK.getAsset('bottomBar', {
anchorX: 0.5,
anchorY: 0
});
topBar.x = SCREEN_WIDTH / 2;
topBar.y = 65;
topBar.scale.y = 0.4;
bottomBarContainer.addChild(topBar);
var moneyText = new Text2('$' + storage.money, {
size: 105,
fill: 0xffd700,
weight: 800,
stroke: 0x000000,
strokeThickness: 4
});
moneyText.anchor.set(1, 0.5);
moneyText.x = SCREEN_WIDTH - 150;
moneyText.y = topBar.y + topBar.height * topBar.scale.y / 2;
bottomBarContainer.addChild(moneyText);
// --- Title screen selection highlight asset ---
var titleScreenSelection = LK.getAsset('titleScreenSelection', {
anchorX: 0.5,
anchorY: 1
});
titleScreenSelection.alpha = 0.3;
bottomBarContainer.addChild(titleScreenSelection);
// --- Track current selection: "battle", "mods", or "shop" ---
var currentTitleScreenSelection = "battle";
// --- Calculate button positions for highlight ---
var playButton = LK.getAsset('playButton', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
playButton.x = SCREEN_WIDTH / 2;
playButton.y = SCREEN_HEIGHT - 100;
// Add battleIcon directly above playButton
var battleIcon = LK.getAsset('battleIcon', {
anchorX: 0.5,
anchorY: 1,
interactive: true
});
battleIcon.x = playButton.x;
battleIcon.y = playButton.y - playButton.height / 2 - 10; // 30px gap above playButton
bottomBarContainer.addChild(battleIcon);
bottomBarContainer.addChild(playButton);
// --- Suit mod button and mods icon ---
var suitModButton = LK.getAsset('suitModButton', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
suitModButton.x = playButton.x + playButton.width / 2 + 100 + suitModButton.width / 2;
suitModButton.y = playButton.y;
// Add modsIcon directly above suitModButton, same orientation as battleIcon
var modsIcon = LK.getAsset('modsIcon', {
anchorX: 0.5,
anchorY: 1,
interactive: true
});
modsIcon.x = suitModButton.x;
modsIcon.y = suitModButton.y - suitModButton.height / 2 - 10; // 10px gap above suitModButton
bottomBarContainer.addChild(modsIcon);
bottomBarContainer.addChild(suitModButton);
// --- Shop button and icon ---
var shopButton = LK.getAsset('shopButton', {
anchorX: 0.5,
anchorY: 0.5,
interactive: true
});
shopButton.x = playButton.x - playButton.width / 2 - 100 - shopButton.width / 2;
shopButton.y = playButton.y + 10;
var shopIcon = LK.getAsset('shopIcon', {
anchorX: 0.5,
anchorY: 1,
interactive: true
});
shopIcon.x = shopButton.x;
shopIcon.y = shopButton.y - shopButton.height / 2 - 10;
bottomBarContainer.addChild(shopIcon);
bottomBarContainer.addChild(shopButton);
// --- Mods container as before ---
var modsContainer = new Container();
modsContainer.visible = false;
uiLayer.addChild(modsContainer);
startScreenElements.push(modsContainer);
// --- Shop Container ---
var shopContainer = new Container();
shopContainer.visible = false;
uiLayer.addChild(shopContainer);
startScreenElements.push(shopContainer);
var shopTitle = new Text2('Shop', {
size: 150,
fill: 0xffffff,
weight: 'bold'
});
shopTitle.anchor.set(0.5, 0.5);
shopTitle.x = SCREEN_WIDTH / 2;
shopTitle.y = 400;
shopContainer.addChild(shopTitle);
// Add black overlay behind the random mod shop item
var shopOverlay = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
var overlayWidth = 800;
var overlayHeight = 1200; // 2:3 ratio
shopOverlay.scale.set(overlayWidth / shopOverlay.width, overlayHeight / shopOverlay.height);
shopOverlay.x = SCREEN_WIDTH / 2;
shopOverlay.y = 1476 - 200;
shopOverlay.tint = 0x000000;
shopOverlay.alpha = 0.8;
shopContainer.addChild(shopOverlay);
// Add shopRandomMod asset in center
var shopRandomModAsset = LK.getAsset('shopRandomMod', {
anchorX: 0.5,
anchorY: 0.5
});
shopRandomModAsset.x = SCREEN_WIDTH / 2;
shopRandomModAsset.y = SCREEN_HEIGHT / 2 - 200;
shopRandomModAsset.scale.set(0.8);
shopContainer.addChild(shopRandomModAsset);
// Add descriptive text underneath
var shopDescText = new Text2('Buy a random card mod', {
size: 60,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 3
});
shopDescText.anchor.set(0.5, 0.5);
shopDescText.x = SCREEN_WIDTH / 2;
shopDescText.y = shopRandomModAsset.y + shopRandomModAsset.height / 2 + 100;
shopContainer.addChild(shopDescText);
// Add price text
var shopPriceText = new Text2('$' + storage.randomModPrice, {
size: 80,
fill: 0xffd700,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 4
});
shopPriceText.anchor.set(0.5, 0.5);
shopPriceText.x = SCREEN_WIDTH / 2;
shopPriceText.y = shopDescText.y + 80;
shopContainer.addChild(shopPriceText);
function buyRandomMod() {
if (storage.money < storage.randomModPrice) {
createFloatingText("Not enough money!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0xff0000, 100);
return;
}
var unownedMods = ModSystem.getUnownedMods();
if (unownedMods.length === 0) {
createFloatingText("All mods unlocked!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0x00ff00, 100);
shopPriceText.setText('SOLD OUT');
shopOverlay.interactive = false;
shopRandomModAsset.interactive = false;
return;
}
storage.money -= storage.randomModPrice;
moneyText.setText('$' + storage.money);
shopOverlay.interactive = false;
shopRandomModAsset.interactive = false;
// Hide the static shop random mod asset and its text
shopRandomModAsset.visible = false;
shopDescText.visible = false;
shopPriceText.visible = false;
var slotContainer = new Container();
// Position it to replace the shopRandomModAsset
slotContainer.x = shopRandomModAsset.x;
slotContainer.y = shopRandomModAsset.y;
shopContainer.addChild(slotContainer);
var displayAsset = null;
var spinDuration = 50;
var spinsLeft = 20 + Math.floor(Math.random() * 15);
// NEW LOGIC: Guarantee one of each suit before being fully random.
// 1. Find which suits the player already owns at least one mod for.
var ownedSuits = {};
for (var ownedModId in ModSystem.ownedMods) {
if (ModSystem.ownedMods[ownedModId]) {
// Check if the value is true
var modData = ModSystem.modData[ownedModId];
if (modData) {
ownedSuits[modData.suit] = true;
}
}
}
// 2. Identify suits for which the player owns no mods.
var suitsWithNoMods = [];
CardSystem.suits.forEach(function (suit) {
if (!ownedSuits[suit]) {
suitsWithNoMods.push(suit);
}
});
// 3. Create a pool of "priority" mods to choose from.
var priorityUnownedMods = [];
if (suitsWithNoMods.length > 0) {
unownedMods.forEach(function (unownedModId) {
var modData = ModSystem.modData[unownedModId];
if (modData && suitsWithNoMods.indexOf(modData.suit) !== -1) {
priorityUnownedMods.push(unownedModId);
}
});
}
var finalModId;
// 4. Decide which pool to pick the final mod from.
if (priorityUnownedMods.length > 0) {
// The player is missing mods for some suits, so guarantee one from that pool.
finalModId = priorityUnownedMods[Math.floor(Math.random() * priorityUnownedMods.length)];
} else {
// The player either has at least one mod for every suit, or there are no unowned mods for the missing suits.
// Fallback to picking any unowned mod.
finalModId = unownedMods[Math.floor(Math.random() * unownedMods.length)];
}
function spin() {
spinsLeft--;
if (displayAsset && displayAsset.parent) {
displayAsset.parent.removeChild(displayAsset);
}
var modIdToShow = spinsLeft <= 0 ? finalModId : unownedMods[Math.floor(Math.random() * unownedMods.length)];
// Scale to fill the space of the shopRandomModAsset
var sizingAsset = LK.getAsset('shopRandomMod', {});
var modAssetForSizing = LK.getAsset(modIdToShow, {});
// Match the visible height of the random mod asset, with a small margin
var scaleToFit = sizingAsset.height * shopRandomModAsset.scale.y / modAssetForSizing.height * 0.9;
displayAsset = LK.getAsset(modIdToShow, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: scaleToFit,
scaleY: scaleToFit
});
slotContainer.addChild(displayAsset);
if (spinsLeft <= 0) {
// REVEAL SEQUENCE
ModSystem.unlockMod(finalModId);
if (!TutorialSystem.isActive) {
storage.randomModPrice = Math.floor(storage.randomModPrice * 1.5) + 50;
}
shopPriceText.setText('$' + storage.randomModPrice);
// Create grey-out overlay
var revealOverlay = new Container();
revealOverlay.interactive = true;
revealOverlay.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
uiLayer.addChild(revealOverlay);
var bg = LK.getAsset('card', {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
anchorX: 0,
anchorY: 0
});
bg.tint = 0x000000;
bg.alpha = 0.8;
revealOverlay.addChild(bg);
// Move the final mod asset to the reveal overlay
var finalModAsset = displayAsset;
finalModAsset.parent.removeChild(finalModAsset);
revealOverlay.addChild(finalModAsset);
finalModAsset.x = SCREEN_WIDTH / 2;
finalModAsset.y = SCREEN_HEIGHT / 2;
// The slot container is now empty, remove it.
if (slotContainer.parent) {
slotContainer.parent.removeChild(slotContainer);
}
// Grow larger animation
tween(finalModAsset, {
scaleX: finalModAsset.scale.x * 1.5,
scaleY: finalModAsset.scale.y * 1.5
}, {
duration: 500,
easing: tween.elasticOut
});
// "You received" message
var modData = ModSystem.modData[finalModId];
var receivedText = new Text2("You received:\n" + modData.name, {
size: 90,
fill: 0xffd700,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 6,
align: 'center'
});
receivedText.anchor.set(0.5, 0.5);
receivedText.x = SCREEN_WIDTH / 2;
receivedText.y = finalModAsset.y + finalModAsset.height * finalModAsset.scale.y / 2 + 150;
revealOverlay.addChild(receivedText);
// Tap to continue message
var tapToContinue = new Text2("Tap to continue", {
size: 60,
fill: 0xffffff,
weight: 'bold'
});
tapToContinue.anchor.set(0.5, 0.5);
tapToContinue.x = SCREEN_WIDTH / 2;
tapToContinue.y = SCREEN_HEIGHT - 200;
revealOverlay.addChild(tapToContinue);
// Set up tap to continue
revealOverlay.down = function () {
revealOverlay.interactive = false; // Prevent multiple taps
// Fade out and remove reveal screen
tween(revealOverlay, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
if (revealOverlay.parent) {
revealOverlay.parent.removeChild(revealOverlay);
}
// Show shop elements again
shopRandomModAsset.visible = true;
shopDescText.visible = true;
shopPriceText.visible = true;
// Re-enable shop interaction if there are mods left
if (ModSystem.getUnownedMods().length > 0) {
shopOverlay.interactive = true;
shopRandomModAsset.interactive = true;
} else {
shopPriceText.setText('SOLD OUT');
}
}
});
};
} else {
if (spinsLeft < 10) {
spinDuration += 25;
}
if (spinsLeft < 5) {
spinDuration += 40;
}
LK.setTimeout(spin, spinDuration);
}
}
spin();
}
shopOverlay.interactive = true;
shopOverlay.down = buyRandomMod;
shopRandomModAsset.interactive = true;
shopRandomModAsset.down = buyRandomMod;
// Add pulsing animation to shopRandomMod
function createPulseAnimation() {
if (!shopRandomModAsset.parent) {
return;
}
tween(shopRandomModAsset, {
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!shopRandomModAsset.parent) {
return;
}
tween(shopRandomModAsset, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: createPulseAnimation
});
}
});
}
createPulseAnimation();
// --- Selection highlight logic ---
// Helper to move the highlight to the correct button
function updateTitleScreenSelectionHighlight() {
var targetX, targetY, targetW, targetH;
if (currentTitleScreenSelection === "battle") {
targetX = playButton.x;
targetY = playButton.y + playButton.height / 2; // anchorY:1, so bottom edge
targetW = playButton.width * 1.1;
targetH = playButton.height + battleIcon.height + 20; // Cover button, icon, and gap with padding
} else if (currentTitleScreenSelection === "mods") {
targetX = suitModButton.x;
targetY = suitModButton.y + suitModButton.height / 2;
targetW = suitModButton.width * 1.1;
targetH = suitModButton.height + modsIcon.height + 20; // Cover button, icon, and gap with padding
} else if (currentTitleScreenSelection === "shop") {
targetX = shopButton.x;
targetY = shopButton.y + shopButton.height / 2;
targetW = shopButton.width * 1.2;
targetH = shopButton.height + shopIcon.height + 20;
}
// Animate movement and scaling for smoothness
tween.stop(titleScreenSelection);
tween(titleScreenSelection, {
x: targetX,
y: targetY,
width: targetW,
height: targetH
}, {
duration: 180,
easing: tween.cubicOut
});
}
// Set initial size and position for highlight (battle by default)
titleScreenSelection.width = playButton.width * 1.2;
titleScreenSelection.height = playButton.height + battleIcon.height + 20;
titleScreenSelection.x = playButton.x;
titleScreenSelection.y = playButton.y + playButton.height / 2;
// --- Selection switching logic ---
// Screens: "battle", "mods", "shop"
playButton.down = function () {
if (currentTitleScreenSelection !== "battle") {
currentTitleScreenSelection = "battle";
updateTitleScreenSelectionHighlight();
}
// Show battle screen, hide mods, shop and logo
modsContainer.visible = false;
shopContainer.visible = false;
if (battleScreenContainer) {
battleScreenContainer.visible = true;
}
gameLogo.visible = false;
};
suitModButton.down = function () {
if (currentTitleScreenSelection === "mods") {
return;
}
if (currentTitleScreenSelection !== "mods") {
currentTitleScreenSelection = "mods";
updateTitleScreenSelectionHighlight();
}
// Show mods, hide others
modsContainer.visible = true;
shopContainer.visible = false;
if (battleScreenContainer) {
battleScreenContainer.visible = false;
}
gameLogo.visible = false;
if (modsContainer.visible) {
ModSystem.updateTopSuitDisplay();
// Refresh mod grid visuals
var gridContainer = modsContainer.children[modsContainer.children.length - 1];
if (gridContainer && gridContainer.children.length > 0) {
var suitModAssets = ['burnHeartMod', 'chipsDiamondMod', 'spreadClubMod', 'freezeSpadeMod', 'unityHeartsMod', 'gamblerDiamondsMod', 'slowClubsMod', 'mineSpadesMod', 'investmentHeartsMod', 'boostDiamondsMod', 'ricochetClubsMod', 'deathSpadesMod'];
var childrenPerMod = 4; // Assumes cell, shadow, mod asset, name text
for (var modIndex = 0; modIndex < suitModAssets.length; modIndex++) {
var assetId = suitModAssets[modIndex];
var isOwned = ModSystem.isModOwned(assetId);
var cellIndex = modIndex * childrenPerMod;
var modAssetIndex = cellIndex + 2;
if (gridContainer.children[cellIndex] && gridContainer.children[modAssetIndex]) {
var cell = gridContainer.children[cellIndex];
var modAsset = gridContainer.children[modAssetIndex];
if (isOwned) {
modAsset.tint = 0xffffff;
modAsset.alpha = 1.0;
cell.tint = 0xffffff;
} else {
modAsset.tint = 0x555555;
modAsset.alpha = 0.6;
cell.tint = 0x888888;
}
}
}
}
}
};
shopButton.down = function () {
if (currentTitleScreenSelection === "shop") {
return;
}
if (currentTitleScreenSelection !== "shop") {
currentTitleScreenSelection = "shop";
updateTitleScreenSelectionHighlight();
}
// Show shop, hide others
shopContainer.visible = true;
modsContainer.visible = false;
if (battleScreenContainer) {
battleScreenContainer.visible = false;
}
gameLogo.visible = false;
};
battleIcon.down = playButton.down;
modsIcon.down = suitModButton.down;
var originalPlayDown = playButton.down;
var originalModsDown = suitModButton.down;
var originalShopDown = shopButton.down;
playButton.down = originalPlayDown;
suitModButton.down = originalModsDown;
shopButton.down = originalShopDown;
battleIcon.down = playButton.down;
modsIcon.down = suitModButton.down;
shopIcon.down = shopButton.down;
var shopItemsForTutorial = {
overlay: shopOverlay,
asset: shopRandomModAsset,
buyButtonDown: buyRandomMod,
priceText: shopPriceText
};
TutorialSystem.registerStartScreenElements({
battle: {
button: playButton,
icon: battleIcon,
down: originalPlayDown
},
mods: {
button: suitModButton,
icon: modsIcon,
down: originalModsDown
},
shop: {
button: shopButton,
icon: shopIcon,
down: originalShopDown
},
modsContainer: modsContainer,
shopContainer: shopContainer,
shopItems: shopItemsForTutorial
});
if (TutorialSystem.isActive && fromMatch) {
// A small delay to ensure the start screen elements are fully rendered
LK.setTimeout(function () {
TutorialSystem.advanceStep();
}, 200);
}
var numCircles = suitAssets.length;
var totalBarWidth = SCREEN_WIDTH - 200;
var suitSpacing = totalBarWidth / numCircles;
var startX = SCREEN_WIDTH / 2 - totalBarWidth / 2 + suitSpacing / 2;
var yOffset = SCREEN_HEIGHT * 0.1;
var circleY = 300 + yOffset;
ModSystem.topSuitGraphics = []; // Reset the array
for (var i = 0; i < numCircles; i++) {
var circleX = startX + i * suitSpacing;
var suitContainer = new Container();
var circleBg = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
circleBg.scale.set(1.5);
suitContainer.addChild(circleBg);
suitContainer.x = circleX;
suitContainer.y = circleY - SCREEN_HEIGHT * 0.05 + 100;
modsContainer.addChild(suitContainer);
ModSystem.topSuitGraphics.push(suitContainer);
}
// --- Dark overlay below the four cards and above the equipped mods text ---
var overlayCard = LK.getAsset('card', {
anchorX: 0.5,
anchorY: 0
});
var overlayWidth = totalBarWidth + 120; // Wide enough for all four cards and some margin
var overlayHeight = 650; // Tall enough to cover the cards and the equipped mods text
overlayCard.scale.set(overlayWidth / overlayCard.width, overlayHeight / overlayCard.height);
overlayCard.x = SCREEN_WIDTH / 2;
overlayCard.y = circleY - SCREEN_HEIGHT * 0.05 - overlayCard.height * overlayCard.scale.y / 2 + 130; // Start just above the cards
overlayCard.alpha = 0.6;
overlayCard.tint = 0x000000;
modsContainer.addChildAt(overlayCard, 0);
var equippedModsText = new Text2('Currently equipped mods', {
size: 45,
fill: 0xffffff,
weight: '600'
});
equippedModsText.anchor.set(0.5, 0);
equippedModsText.x = SCREEN_WIDTH / 2;
equippedModsText.y = circleY - SCREEN_HEIGHT * 0.05 + 350; // Position 25px below the mod icons
modsContainer.addChild(equippedModsText);
var gridContainer = new Container();
gridContainer.x = SCREEN_WIDTH / 2;
gridContainer.y = circleY + 450 - SCREEN_HEIGHT * 0.05;
modsContainer.addChild(gridContainer);
var cols = 4;
var rows = 3;
var cardForSizing = LK.getAsset('card', {});
var cellWidth = cardForSizing.width;
var cellHeight = cardForSizing.height;
var colSpacing = 160;
var rowSpacing = 140;
var gridTotalWidth = cols * cellWidth + (cols - 1) * colSpacing;
var gridStartX = -gridTotalWidth / 2;
var gridStartY = 50;
var suitModAssets = ['burnHeartMod', 'chipsDiamondMod', 'spreadClubMod', 'freezeSpadeMod', 'unityHeartsMod', 'gamblerDiamondsMod', 'slowClubsMod', 'mineSpadesMod', 'investmentHeartsMod', 'boostDiamondsMod', 'ricochetClubsMod', 'deathSpadesMod'];
for (var r = 0; r < rows; r++) {
for (var c = 0; c < cols; c++) {
var cell = LK.getAsset('card', {
anchorX: 0,
anchorY: 0
});
cell.scale.set(1.3);
cell.x = gridStartX + c * (cellWidth + colSpacing);
cell.y = gridStartY + r * (cellHeight + rowSpacing);
gridContainer.addChild(cell);
var modIndex = r * cols + c;
if (modIndex < suitModAssets.length) {
var modAssetId = suitModAssets[modIndex];
if (modAssetId) {
var shadow = LK.getAsset('suitShadow', {
anchorX: 0.5,
anchorY: 0.5
});
shadow.x = cell.x + cell.width * cell.scale.x / 2;
shadow.y = cell.y + cell.height * cell.scale.y - cell.height * cell.scale.y / 5 + 15;
shadow.alpha = 0.3;
gridContainer.addChild(shadow);
var modAsset = LK.getAsset(modAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
modAsset.x = cell.x + cell.width * cell.scale.x / 2;
modAsset.y = cell.y + cell.height * cell.scale.y / 2;
var isOwned = ModSystem.isModOwned(modAssetId);
if (!isOwned) {
modAsset.tint = 0x555555;
modAsset.alpha = 0.6;
cell.tint = 0x888888;
}
gridContainer.addChild(modAsset);
var modData = ModSystem.modData[modAssetId];
if (modData) {
var nameText = new Text2(modData.name.toUpperCase(), {
size: 35,
fill: 0x000000,
weight: 'bold',
strokeThickness: 0
});
nameText.anchor.set(0.5, 0);
nameText.x = cell.x + cell.width * cell.scale.x / 2;
nameText.y = cell.y + 20;
gridContainer.addChild(nameText);
}
createModFloatAnimation(modAsset, shadow);
// Add click handler for mod display to the entire cell card
cell.interactive = true;
(function (assetId, modAssetRef) {
cell.down = function () {
ModSystem.showModDisplay(assetId, modAssetRef.x, modAssetRef.y);
};
})(modAssetId, modAsset);
}
}
}
}
currentGameState = 'start';
}
function startGame(isTutorial, mode) {
gameMode = mode || 'pvp';
LK.stopMusic();
// Clear start screen elements
startScreenElements.forEach(function (element) {
if (element.parent) {
element.parent.removeChild(element);
}
});
startScreenElements = [];
backgroundSuits = [];
// Show game elements
gameElements.forEach(function (element) {
element.visible = true;
});
if (gameMode === 'coop') {
background.visible = false;
coopBackground.visible = true;
} else {
background.visible = true;
coopBackground.visible = false;
}
currentGameState = 'playing';
initializeGame(isTutorial || false);
}
function initializeGame(isTutorial) {
// Disable deal button at the start of the round
discardAreaContainer.interactive = false;
discardAreaGraphic.tint = 0x666666;
discardAreaGraphic.alpha = 1.0;
ModSystem.init();
AISystem.setupMods();
PoolManager.init();
PathSystem.init();
// Initialize play areas
gameState.playerPlayArea = [];
gameState.aiPlayArea = [];
playerHandNameTexts = [null, null];
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
gameState.playerPlayArea[row] = [];
gameState.aiPlayArea[row] = [];
for (var col = 0; col < PLAY_AREA_COLS; col++) {
gameState.playerPlayArea[row][col] = null;
gameState.aiPlayArea[row][col] = null;
}
}
// Create initial decks
gameState.playerDeck = CardSystem.createDeck();
gameState.aiDeck = CardSystem.createDeck();
// Draw grid lines
drawPlayAreas();
drawPaths();
createLifeDisplays();
lastPlayerLives = gameState.playerLives;
if (gameMode === 'coop') {
gameState.aiLives = gameState.playerLives;
playerTotalDamage = 0;
aiTotalDamage = 0;
}
lastAiLives = gameState.aiLives;
// Initialize player's hand with empty slots
gameState.playerHand = [null, null, null, null, null];
// Center bar removed
if (isTutorial) {
LK.playMusic('pvpMusic');
gameReady = false; // Don't start waves automatically
AISystem.thinkDelay = 999999; // Effectively disable AI
TutorialSystem.startInGameTutorial();
} else {
// Start the wave system with a "Get Ready" message
var getReadyText = new Text2('Get ready…', {
size: 150,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 10
});
getReadyText.anchor.set(0.5, 0.5);
getReadyText.x = SCREEN_WIDTH / 2;
getReadyText.y = SCREEN_HEIGHT / 2 - 155;
getReadyText.alpha = 0;
uiLayer.addChild(getReadyText);
tween(getReadyText, {
alpha: 1
}, {
duration: 500,
onFinish: function onFinish() {
LK.getSound('getReady').play();
}
});
gameReady = false;
// After 2 seconds, display "Deal 'em!" and begin the first wave.
LK.setTimeout(function () {
if (!getReadyText.parent) {
return;
}
getReadyText.setText('Deal \'em!');
LK.getSound('gameStart').play();
// Deal initial hand
dealInitialHand();
// Remove the message after it has been displayed for a short time
LK.setTimeout(function () {
if (getReadyText.parent) {
tween(getReadyText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (getReadyText.parent) {
getReadyText.parent.removeChild(getReadyText);
}
gameReady = true;
LK.playMusic('pvpMusic');
}
});
}
}, 1000);
}, 2000);
}
}
function createLifeDisplays() {
// Clear any existing hearts and labels
if (opponentNameText && opponentNameText.parent) {
opponentNameText.parent.removeChild(opponentNameText);
}
if (playerNameText && playerNameText.parent) {
playerNameText.parent.removeChild(playerNameText);
}
playerLifeHearts.forEach(function (h) {
if (h.parent) {
h.parent.removeChild(h);
}
});
playerLifeHearts = [];
aiLifeHearts.forEach(function (h) {
if (h.parent) {
h.parent.removeChild(h);
}
});
aiLifeHearts = [];
if (gameMode === 'coop') {
var heartSpacing = 110;
var heartScale = 0.5;
// Position to the right of the coop background, adjusted up and left
var startX = coopBackground.x + coopBackground.width / 2 + 80 - SCREEN_WIDTH * 0.05;
var startY = SCREEN_HEIGHT / 2 - heartSpacing - SCREEN_HEIGHT * 0.07;
// Player lives are unified in coop mode
for (var i = 0; i < gameState.playerLives; i++) {
var heart = LK.getAsset('heartSuit', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: heartScale,
scaleY: heartScale
});
heart.x = startX;
heart.y = startY + i * heartSpacing;
uiLayer.addChild(heart);
playerLifeHearts.push(heart);
}
} else {
var heartSpacing = 110;
var heartScale = 0.5;
var startX_AI = 200 - 70;
var startX_Player = SCREEN_WIDTH - 200 + 70;
var yPos = SCREEN_HEIGHT / 2 - 137 + 10;
var labelYPos = yPos - 60;
// Opponent Name Text
var totalAIHeartsWidth = (gameState.aiLives - 1) * heartSpacing;
opponentNameText = new Text2('Opponent', {
size: 40,
fill: 0xffffff,
weight: 800
});
opponentNameText.anchor.set(0.5, 1);
opponentNameText.x = startX_AI + totalAIHeartsWidth / 2;
opponentNameText.y = labelYPos;
uiLayer.addChild(opponentNameText);
// Player Name Text
var totalPlayerHeartsWidth = (gameState.playerLives - 1) * heartSpacing;
playerNameText = new Text2('Player', {
size: 40,
fill: 0xffffff,
weight: 800
});
playerNameText.anchor.set(0.5, 1);
playerNameText.x = startX_Player - totalPlayerHeartsWidth / 2;
playerNameText.y = labelYPos;
uiLayer.addChild(playerNameText);
// AI Lives (left side)
for (var i = 0; i < gameState.aiLives; i++) {
var heart = LK.getAsset('heartSuit', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: heartScale,
scaleY: heartScale
});
heart.x = startX_AI + i * heartSpacing;
heart.y = yPos;
uiLayer.addChild(heart);
aiLifeHearts.push(heart);
}
// Player Lives (right side)
for (var i = 0; i < gameState.playerLives; i++) {
var heart = LK.getAsset('heartSuit', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: heartScale,
scaleY: heartScale
});
heart.x = startX_Player - i * heartSpacing;
heart.y = yPos;
uiLayer.addChild(heart);
playerLifeHearts.push(heart);
}
}
}
function dealNewHand() {
if (gameState.playerChips < gameState.dealCost) {
return;
}
// Find an empty slot in the player's hand
var emptySlotIndex = -1;
for (var i = 0; i < gameState.playerHand.length; i++) {
if (!gameState.playerHand[i]) {
emptySlotIndex = i;
break;
}
}
// If hand is full, do nothing
if (emptySlotIndex === -1) {
return;
}
gameState.playerChips -= gameState.dealCost;
gameState.dealCount++;
// Progressive cost increase with diminishing returns
var baseCost = 25; // Changed from 38 to 25
var newCost;
if (gameState.dealCount <= 10) {
// First 10 deals: 8% increase each time
newCost = Math.floor(baseCost * Math.pow(1.08, gameState.dealCount));
} else if (gameState.dealCount <= 25) {
// Deals 11-25: 5% increase each time
var cost10 = Math.floor(baseCost * Math.pow(1.08, 10)); // Cost at deal 10
var additionalDeals = gameState.dealCount - 10;
newCost = Math.floor(cost10 * Math.pow(1.05, additionalDeals));
} else {
// Deals 26+: 3% increase each time
var cost10 = Math.floor(baseCost * Math.pow(1.08, 10));
var cost25 = Math.floor(cost10 * Math.pow(1.05, 15)); // 15 deals at 5%
var additionalDeals = gameState.dealCount - 25;
newCost = Math.floor(cost25 * Math.pow(1.03, additionalDeals));
}
gameState.dealCost = newCost;
if (gameState.playerDeck.length === 0) {
gameState.playerDeck = CardSystem.createDeck();
}
var cardData = gameState.playerDeck.pop();
// After every 10 waves, new cards dealt start one level higher
var startLevel = Math.floor((WaveSystem.waveNumber - 1) / 10) + 1;
var i = emptySlotIndex;
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
var slotX = handStartX + i * DEAL_SLOT_WIDTH + i * 30 + DEAL_SLOT_WIDTH / 2;
var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2;
// Start position - off to the bottom
var startX = slotX + (Math.random() - 0.5) * 50; // Slight horizontal variance
var startY = SCREEN_HEIGHT + DEAL_SLOT_HEIGHT;
// Mark slot as occupied immediately to prevent overwriting if deal is pressed too quickly
gameState.playerHand[i] = true;
var cardBack = LK.getAsset('cardBack', {
anchorX: 0.5,
anchorY: 0.5
});
cardBack.x = startX;
cardBack.y = startY;
uiLayer.addChild(cardBack);
// Animate card flying into place and flipping
tween(cardBack, {
x: slotX,
y: slotY
}, {
duration: 250,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Flip animation: shrink back
tween(cardBack, {
scaleX: 0.01
}, {
duration: 150,
easing: tween.quadIn,
onFinish: function onFinish() {
if (cardBack.parent) {
cardBack.parent.removeChild(cardBack);
}
// Create and show the real card
var card = new Card(cardData);
card.setLevel(startLevel);
card.activate(slotX, slotY, false, true);
card.scale.x = 0.01;
uiLayer.addChild(card);
// Replace the placeholder with the actual card object
gameState.playerHand[i] = card;
// Flip animation: expand card face
tween(card, {
scaleX: 1.0
}, {
duration: 150,
easing: tween.quadOut,
onFinish: function onFinish() {
LK.getSound('cardLand').play();
}
});
}
});
}
});
updateUI();
}
function updateUI() {
playerChipsText.setText('Chips: ' + formatNumberWithSuffix(gameState.playerChips));
// Update combined deal/discard button text and appearance when not dragging
if (!isDragging) {
discardText.setText('-' + formatNumberWithSuffix(gameState.dealCost));
discardText.fill = 0xffffff;
// Update button color based on affordability
if (gameState.playerChips >= gameState.dealCost) {
discardAreaGraphic.tint = 0xffffff; // No tint when affordable
discardAreaGraphic.alpha = 1.0; // Full alpha when affordable
} else {
discardAreaGraphic.tint = 0x666666; // Grey tint when not affordable
discardAreaGraphic.alpha = 1.0; // Keep full alpha even when not affordable
}
}
waveText.setText('Wave: ' + WaveSystem.waveNumber);
}
function drawPlayAreas() {
// Draw player play area slots
var hand1Text = 'HAND1';
var hand2Text = 'HAND2';
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var slot = new Container();
var slotGraphics = slot.attachAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
slotGraphics.alpha = 0.5;
var textToShow = row === 0 ? hand1Text : hand2Text;
var text = new Text2(textToShow.charAt(col), {
size: 180,
fill: 0xffffff,
weight: 'bold'
});
text.anchor.set(0.5);
text.alpha = 0.3;
slot.addChild(text);
slot.x = PLAYER_AREA_X + col * SLOT_WIDTH + SLOT_WIDTH / 2;
slot.y = PLAYER_AREA_Y + row * SLOT_HEIGHT + SLOT_HEIGHT / 2;
gameLayer.addChild(slot);
slotIndicators.push(slot);
}
}
// Draw AI play area slots
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var slot = new Container();
var slotGraphics = slot.attachAsset('card', {
anchorX: 0.5,
anchorY: 0.5
});
slotGraphics.alpha = 0.5;
slotGraphics.tint = 0xff8888; // Red tint for AI area
var textToShow = row === 0 ? hand1Text : hand2Text;
var text = new Text2(textToShow.charAt(col), {
size: 180,
fill: 0xffffff,
weight: 'bold'
});
text.anchor.set(0.5);
text.alpha = 0.3;
slot.addChild(text);
slot.x = AI_AREA_X + col * SLOT_WIDTH + SLOT_WIDTH / 2;
slot.y = AI_AREA_Y + row * SLOT_HEIGHT + SLOT_HEIGHT / 2;
gameLayer.addChild(slot);
slotIndicators.push(slot);
}
}
// Draw player deal area slots (hand)
var rating = storage.battleRating;
var ratingString = String(rating);
while (ratingString.length < 4) {
ratingString = '0' + ratingString;
}
for (var i = 0; i < 5; i++) {
var dealSlot = new Container();
var dealSlotGraphics = dealSlot.attachAsset('dealSlot', {
anchorX: 0.5,
anchorY: 0.5
});
dealSlotGraphics.alpha = 0.5;
if (i === 0) {
var icon = dealSlot.attachAsset('battleRatingIcon', {
anchorX: 0.5,
anchorY: 0.5
});
icon.alpha = 0.3;
var iconScale = dealSlotGraphics.width * 0.8 / icon.width;
icon.scale.set(iconScale);
} else {
var digit = ratingString.charAt(i - 1);
var text = new Text2(digit, {
size: 180,
fill: 0xffffff,
weight: 'bold'
});
text.anchor.set(0.5);
text.alpha = 0.3;
dealSlot.addChild(text);
}
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
dealSlot.x = handStartX + i * DEAL_SLOT_WIDTH + i * 30 + DEAL_SLOT_WIDTH / 2;
dealSlot.y = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2;
gameLayer.addChild(dealSlot);
slotIndicators.push(dealSlot);
}
}
function drawPaths() {
// Path lines removed - enemies still follow invisible paths
}
// drawPathSegment function removed - no longer needed since path lines are removed
function getSlotPosition(row, col, isPlayerArea) {
var baseX = isPlayerArea ? PLAYER_AREA_X : AI_AREA_X;
var baseY = isPlayerArea ? PLAYER_AREA_Y : AI_AREA_Y;
return {
x: baseX + col * SLOT_WIDTH + SLOT_WIDTH / 2,
y: baseY + row * SLOT_HEIGHT + SLOT_HEIGHT / 2
};
}
function getSlotFromPosition(x, y) {
// Check player play area
if (x >= PLAYER_AREA_X && x <= PLAYER_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH && y >= PLAYER_AREA_Y && y <= PLAYER_AREA_Y + PLAY_AREA_ROWS * SLOT_HEIGHT) {
var col = Math.floor((x - PLAYER_AREA_X) / SLOT_WIDTH);
var row = Math.floor((y - PLAYER_AREA_Y) / SLOT_HEIGHT);
if (col >= 0 && col < PLAY_AREA_COLS && row >= 0 && row < PLAY_AREA_ROWS) {
return {
area: 'player',
row: row,
col: col
};
}
}
// Check player hand area
if (y >= PLAYER_DEAL_AREA_Y && y <= PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT) {
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
for (var i = 0; i < 5; i++) {
var slotXStart = handStartX + i * (DEAL_SLOT_WIDTH + 30);
var slotXEnd = slotXStart + DEAL_SLOT_WIDTH;
if (x >= slotXStart && x <= slotXEnd) {
return {
area: 'hand',
index: i
};
}
}
}
// Check discard area
if (discardAreaContainer && x >= discardAreaContainer.x - DEAL_SLOT_WIDTH / 2 && x <= discardAreaContainer.x + DEAL_SLOT_WIDTH / 2 && y >= discardAreaContainer.y - DEAL_SLOT_HEIGHT / 2 && y <= discardAreaContainer.y + DEAL_SLOT_HEIGHT / 2) {
return {
area: 'discard'
};
}
return null;
}
function evaluateRowHand(row, isPlayerArea) {
var playArea = isPlayerArea ? gameState.playerPlayArea : gameState.aiPlayArea;
var cards = [];
for (var col = 0; col < PLAY_AREA_COLS; col++) {
if (playArea[row][col]) {
cards.push(playArea[row][col]);
}
}
return CardSystem.evaluatePokerHand(cards);
}
function updateHandNameDisplay(row, handEval) {
var existingText = playerHandNameTexts[row];
var shouldShowText = handEval.strength > 1;
if (shouldShowText) {
var handName = handEval.type.replace(/_/g, ' ').toUpperCase();
if (existingText) {
if (existingText.text !== handName) {
existingText.setText(handName);
}
existingText.visible = true;
} else {
var newText = new Text2(handName, {
size: 50,
fill: 0xffffff,
weight: '800',
stroke: 0x000000,
strokeThickness: 0
});
newText.anchor.set(0.5, 0);
newText.alpha = 0.8;
newText.x = PLAYER_AREA_X + PLAY_AREA_COLS * SLOT_WIDTH / 2;
newText.y = PLAYER_AREA_Y + (row + 1) * SLOT_HEIGHT - 20;
uiLayer.addChild(newText);
playerHandNameTexts[row] = newText;
}
} else {
if (existingText) {
existingText.visible = false;
}
}
}
function applyHandBonuses() {
// Apply bonuses to player cards
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
var handEval = evaluateRowHand(row, true);
updateHandNameDisplay(row, handEval);
var contributingCards = handEval.contributingCards || [];
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = gameState.playerPlayArea[row][col];
if (card) {
card.handBonus = handEval.multiplier;
card.calculateStats();
card.redOutline.visible = handEval.strength > 1 && contributingCards.indexOf(card) !== -1;
}
}
}
// Apply bonuses to AI cards
AISystem.applyAIHandBonuses();
}
function getAIDifficultySettings(battleRating) {
// Post-tutorial AI intelligence now scales more aggressively with battle rating.
// Normalize rating from 0 to 1, clamped between 0 and a max rating of 1500 (was 2000).
var maxRatingForScaling = 1500;
var normalizedRating = Math.max(0, Math.min(battleRating || 0, maxRatingForScaling)) / maxRatingForScaling;
// thinkDelay: from 300 (5s) down to 45 (0.75s). Starts faster than the original 360 (6s).
var thinkDelay = 300 - normalizedRating * (300 - 45);
// optimizationChance: from 10% up to 80%. Higher baseline and ceiling (was 5%-70%).
var optimizationChance = 0.1 + normalizedRating * (0.8 - 0.1);
// shouldOptimize: enabled above 50 rating (was 100).
var shouldOptimize = (battleRating || 0) > 50;
return {
thinkDelay: Math.floor(thinkDelay),
optimizationChance: optimizationChance,
shouldOptimize: shouldOptimize,
canLevelUp: (battleRating || 0) > 0
};
}
/****
* AI System
****/
var AISystem = {
thinkTimer: 0,
update: function update() {
var difficulty = getAIDifficultySettings(storage.battleRating);
var currentThinkDelay = TutorialSystem.isActive ? 240 : difficulty.thinkDelay;
this.thinkTimer++;
if (this.thinkTimer >= currentThinkDelay) {
this.thinkTimer = 0;
this.makeMove();
}
},
shouldDeal: function shouldDeal() {
// Deal if we can afford it and have empty slots
if (gameState.aiChips < gameState.dealCost) {
return false;
}
var emptySlots = this.countEmptySlots();
// Deal if there are any empty slots at all
return emptySlots > 0;
},
countEmptySlots: function countEmptySlots() {
var count = 0;
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
if (!gameState.aiPlayArea[row][col]) {
count++;
}
}
}
return count;
},
countLowLevelCards: function countLowLevelCards() {
var count = 0;
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = gameState.aiPlayArea[row][col];
if (card && card.level <= 2) {
count++;
}
}
}
return count;
},
makeMove: function makeMove() {
var difficulty = getAIDifficultySettings(storage.battleRating);
// Priority order: Merge > Place > Optimize > Deal. Only one action per cycle.
if (difficulty.canLevelUp && this.tryMergeCards()) {
// Successfully merged, wait for next think cycle
return;
}
if (this.tryPlaceCards()) {
// Successfully placed cards
return;
}
if (difficulty.shouldOptimize && this.optimizeCardPositions()) {
// Successfully optimized, wait for next think cycle
return;
}
// As a last resort, try to deal a new card if possible
if (this.shouldDeal()) {
this.dealAIHand();
}
},
tryMergeCards: function tryMergeCards() {
// Look for mergeable cards
for (var row1 = 0; row1 < PLAY_AREA_ROWS; row1++) {
for (var col1 = 0; col1 < PLAY_AREA_COLS; col1++) {
var card1 = gameState.aiPlayArea[row1][col1];
if (!card1) {
continue;
}
// Look for a card to merge with
for (var row2 = 0; row2 < PLAY_AREA_ROWS; row2++) {
for (var col2 = 0; col2 < PLAY_AREA_COLS; col2++) {
if (row1 === row2 && col1 === col2) {
continue;
}
var card2 = gameState.aiPlayArea[row2][col2];
if (!card2) {
continue;
}
if (card1.canMergeWith(card2)) {
this.mergeCards(card1, card2, row1, col1, row2, col2);
return true;
}
}
}
}
}
return false;
},
mergeCards: function mergeCards(card1, card2, row1, col1, row2, col2) {
var mergedCard = card1.mergeWith(card2);
if (mergedCard) {
var oldLevel = card2.level;
// Remove old cards
gameLayer.removeChild(card1);
gameLayer.removeChild(card2);
gameState.aiPlayArea[row1][col1] = null;
gameState.aiPlayArea[row2][col2] = null;
// Place merged card in the first position
var pos = getSlotPosition(row1, col1, false);
mergedCard.activate(pos.x, pos.y, true, false);
gameLayer.addChild(mergedCard);
gameState.aiPlayArea[row1][col1] = mergedCard;
// Merge animation
mergedCard.alpha = 0;
mergedCard.scaleX = mergedCard.scaleY = 1.5;
tween(mergedCard, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.elasticOut
});
createFloatingText('+Level Up!', mergedCard.x, mergedCard.y - 50, 0x00ff00, 60);
var damageBefore = Math.floor(10 * Math.pow(1.6, oldLevel - 1));
var fireRateBefore = Math.max(15, Math.floor(60 / Math.pow(1.2, oldLevel - 1)));
var damageAfter = Math.floor(10 * Math.pow(1.6, mergedCard.level - 1));
var fireRateAfter = Math.max(15, Math.floor(60 / Math.pow(1.2, mergedCard.level - 1)));
var damageIncrease = damageBefore > 0 ? Math.round((damageAfter / damageBefore - 1) * 100) : 0;
var fireRateIncrease = fireRateAfter > 0 && fireRateAfter < fireRateBefore ? Math.round((fireRateBefore / fireRateAfter - 1) * 100) : 0;
var statText = "";
if (damageBefore > 0) {
var dmgMult = (damageAfter / damageBefore).toFixed(2);
statText = "Dmg x" + dmgMult;
}
createFloatingText(statText, mergedCard.x, mergedCard.y + 55, 0x00ff00, 35);
this.applyAIHandBonuses();
}
},
tryPlaceCards: function tryPlaceCards() {
// AI doesn't have a "hand" like player, it deals directly to board
// This function is for future expansion
return false;
},
optimizeCardPositions: function optimizeCardPositions() {
// Move stronger cards to better positions (like completing poker hands)
// For now, just try to complete rows for hand bonuses
return this.tryCompletePokerHands();
},
tryCompletePokerHands: function tryCompletePokerHands() {
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
var rowCards = [];
var emptyPositions = [];
for (var col = 0; col < PLAY_AREA_COLS; col++) {
if (gameState.aiPlayArea[row][col]) {
rowCards.push({
card: gameState.aiPlayArea[row][col],
col: col
});
} else {
emptyPositions.push(col);
}
}
// If row is almost complete, try to fill it strategically
if (rowCards.length >= 3 && emptyPositions.length > 0) {
// Look for cards in other rows that might complete a hand
if (this.tryMoveCardToCompleteHand(row, rowCards, emptyPositions[0])) {
return true; // A move was made, so we are done for this 'think' cycle
}
}
}
return false; // No move was made
},
tryMoveCardToCompleteHand: function tryMoveCardToCompleteHand(targetRow, existingCards, targetCol) {
var difficulty = getAIDifficultySettings(storage.battleRating);
// Look for cards in other positions that might help complete a hand
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
if (row === targetRow) {
continue;
}
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = gameState.aiPlayArea[row][col];
if (!card) {
continue;
}
// Simple heuristic: move cards of same suit or sequential values
var shouldMove = this.cardHelpsHand(card, existingCards);
if (shouldMove && Math.random() < difficulty.optimizationChance) {
// Dynamic chance to move
// Move the card
gameState.aiPlayArea[row][col] = null;
gameState.aiPlayArea[targetRow][targetCol] = card;
var newPos = getSlotPosition(targetRow, targetCol, false);
tween(card, {
x: newPos.x,
y: newPos.y
}, {
duration: 300,
easing: tween.quadOut
});
this.applyAIHandBonuses();
return true;
}
}
}
return false;
},
cardHelpsHand: function cardHelpsHand(card, existingCards) {
// Simple heuristic to see if a card might help complete a poker hand
var suits = {};
var values = {};
existingCards.forEach(function (cardInfo) {
var c = cardInfo.card.cardData;
suits[c.suit] = (suits[c.suit] || 0) + 1;
values[c.value] = (values[c.value] || 0) + 1;
});
// Check if card matches existing suits or values
var cardSuit = card.cardData.suit;
var cardValue = card.cardData.value;
return suits[cardSuit] >= 2 || values[cardValue] >= 1;
},
dealAIHand: function dealAIHand() {
if (gameState.aiChips < gameState.dealCost) {
return;
}
gameState.aiChips -= gameState.dealCost;
// Deal one card to an empty slot
var cardData;
// AI cannot play jokers, so re-deal if one is drawn.
do {
if (gameState.aiDeck.length === 0) {
gameState.aiDeck = CardSystem.createDeck();
}
cardData = gameState.aiDeck.pop();
} while (cardData.suit === 'joker');
// After every 10 waves, new cards dealt start one level higher
var startLevel = Math.floor((WaveSystem.waveNumber - 1) / 10) + 1;
// Find best empty slot (prefer completing rows)
var bestSlot = this.findBestEmptySlot();
if (bestSlot) {
var self = this;
var pos = getSlotPosition(bestSlot.row, bestSlot.col, false);
// Set initial off-screen position and properties for animation
var startY = -SLOT_HEIGHT; // Come from top of the screen
var aiCardBack = LK.getAsset('aiCardBack', {
anchorX: 0.5,
anchorY: 0.5
});
aiCardBack.x = pos.x;
aiCardBack.y = startY;
aiCardBack.rotation = Math.PI * 4; // Two full spins
aiCardBack.scale.set(0.1); // Start small
gameLayer.addChild(aiCardBack);
// Deal animation: spin into place
tween(aiCardBack, {
y: pos.y,
rotation: 0,
scaleX: 1,
scaleY: 1
}, {
duration: 600,
easing: tween.quadOut,
onFinish: function onFinish() {
// Flip animation: shrink back
tween(aiCardBack, {
scaleX: 0.01
}, {
duration: 150,
easing: tween.quadIn,
onFinish: function onFinish() {
if (aiCardBack.parent) {
aiCardBack.parent.removeChild(aiCardBack);
}
// Create and show the real card
var card = new Card(cardData);
card.setLevel(startLevel);
card.activate(pos.x, pos.y, true, false);
card.scale.x = 0.01;
gameLayer.addChild(card);
gameState.aiPlayArea[bestSlot.row][bestSlot.col] = card;
// Flip animation: expand card face
tween(card, {
scaleX: 1.0
}, {
duration: 150,
easing: tween.quadOut,
onFinish: function onFinish() {
LK.getSound('cardLand').play();
self.applyAIHandBonuses();
}
});
}
});
}
});
}
},
findBestEmptySlot: function findBestEmptySlot() {
var emptySlots = [];
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
if (!gameState.aiPlayArea[row][col]) {
var score = this.evaluateSlotScore(row, col);
emptySlots.push({
row: row,
col: col,
score: score
});
}
}
}
if (emptySlots.length === 0) {
return null;
}
// Sort by score (higher is better)
emptySlots.sort(function (a, b) {
return b.score - a.score;
});
return emptySlots[0];
},
evaluateSlotScore: function evaluateSlotScore(row, col) {
var score = 0;
var cardsInRow = 0;
// Count cards in this row
for (var c = 0; c < PLAY_AREA_COLS; c++) {
if (gameState.aiPlayArea[row][c]) {
cardsInRow++;
}
}
// Prefer completing rows
score += cardsInRow * 10;
// Slight preference for middle positions
score += (2 - Math.abs(col - 2)) * 2;
return score;
},
applyAIHandBonuses: function applyAIHandBonuses() {
// Apply poker hand bonuses to AI cards
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
var handEval = evaluateRowHand(row, false);
var contributingCards = handEval.contributingCards || [];
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = gameState.aiPlayArea[row][col];
if (card) {
card.handBonus = handEval.multiplier;
card.calculateStats();
card.redOutline.visible = handEval.strength > 1 && contributingCards.indexOf(card) !== -1;
}
}
}
},
setupMods: function setupMods() {
var playerModCount = 0;
for (var suit in ModSystem.equippedMods) {
if (ModSystem.equippedMods[suit]) {
playerModCount++;
}
}
var allMods = Object.keys(ModSystem.modData);
var availableModsBySuit = {
hearts: [],
diamonds: [],
clubs: [],
spades: []
};
allMods.forEach(function (modId) {
var suit = ModSystem.modData[modId].suit;
if (suit && availableModsBySuit[suit]) {
availableModsBySuit[suit].push(modId);
}
});
ModSystem.aiEquippedMods = {
hearts: null,
diamonds: null,
clubs: null,
spades: null
};
var aiModCount = 0;
var suits = ['hearts', 'diamonds', 'clubs', 'spades'];
for (var i = suits.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = suits[i];
suits[i] = suits[j];
suits[j] = temp;
}
for (var i = 0; i < suits.length; i++) {
var suit = suits[i];
if (aiModCount < playerModCount && availableModsBySuit[suit].length > 0) {
var randomModForSuit = availableModsBySuit[suit][Math.floor(Math.random() * availableModsBySuit[suit].length)];
ModSystem.aiEquippedMods[suit] = randomModForSuit;
aiModCount++;
}
}
}
};
/****
* Input Handling
****/
game.down = function (x, y, obj) {
// Only handle input during playing state
if (currentGameState !== 'playing') {
return;
}
// Check if clicking on a card in hand
for (var i = 0; i < gameState.playerHand.length; i++) {
var card = gameState.playerHand[i];
if (card && Math.abs(x - card.x) < DEAL_SLOT_WIDTH / 2 && Math.abs(y - card.y) < DEAL_SLOT_HEIGHT / 2) {
selectedCard = card;
isDragging = true;
originalCardPosition = {
area: 'hand',
index: i
};
uiLayer.addChild(selectedCard);
return;
}
}
// Check if clicking on a card in play area
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = gameState.playerPlayArea[row][col];
if (card && Math.abs(x - card.x) < SLOT_WIDTH / 2 && Math.abs(y - card.y) < SLOT_HEIGHT / 2) {
selectedCard = card;
isDragging = true;
originalCardPosition = {
area: 'player',
row: row,
col: col
};
// Remove from current position
gameState.playerPlayArea[row][col] = null;
gameLayer.addChild(selectedCard);
return;
}
}
}
};
game.move = function (x, y, obj) {
// Only handle input during playing state
if (currentGameState !== 'playing') {
return;
}
if (isDragging && selectedCard) {
selectedCard.x = x;
selectedCard.y = y;
// Highlight mergeable cards
// Player's play area
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = gameState.playerPlayArea[row][col];
if (card) {
card.greenOutline.visible = selectedCard.canMergeWith(card);
}
}
}
// Player's hand
for (var i = 0; i < gameState.playerHand.length; i++) {
var card = gameState.playerHand[i];
if (card && card !== selectedCard) {
card.greenOutline.visible = selectedCard.canMergeWith(card);
}
}
// Switch to discard mode when dragging
discardAreaGraphic.visible = false;
refundButtonGraphic.visible = true;
// Highlight discard area on hover
var isOverDiscard = discardAreaContainer && x >= discardAreaContainer.x - DEAL_SLOT_WIDTH / 2 && x <= discardAreaContainer.x + DEAL_SLOT_WIDTH / 2 && y >= discardAreaContainer.y - DEAL_SLOT_HEIGHT / 2 && y <= discardAreaContainer.y + DEAL_SLOT_HEIGHT / 2;
var chipRefund = Math.floor(gameState.dealCost / 2);
discardText.setText('+' + formatNumberWithSuffix(chipRefund));
if (isOverDiscard) {
discardText.fill = 0xffd700; // Gold color
refundButtonGraphic.alpha = 1.0;
} else {
refundButtonGraphic.alpha = 0.7;
discardText.fill = 0x999999;
}
} else {
// When not dragging, show as deal button
discardAreaGraphic.visible = true;
refundButtonGraphic.visible = false;
updateUI();
}
};
game.up = function (x, y, obj) {
// Only handle input during playing state
if (currentGameState !== 'playing') {
return;
}
if (isDragging && selectedCard) {
isDragging = false;
discardAreaGraphic.visible = true;
refundButtonGraphic.visible = false;
// Clear all temporary green outlines
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
var card = gameState.playerPlayArea[row][col];
if (card) {
card.greenOutline.visible = false;
}
}
}
for (var i = 0; i < gameState.playerHand.length; i++) {
var card = gameState.playerHand[i];
if (card) {
card.greenOutline.visible = false;
}
}
// Reset to deal button mode
updateUI();
var targetSlot = getSlotFromPosition(x, y);
if (targetSlot) {
// Handle dropping card on discard area
if (targetSlot.area === 'discard') {
var chipRefund = Math.floor(gameState.dealCost / 2);
gameState.playerChips += chipRefund;
// Remove from original position data
if (originalCardPosition.area === 'hand') {
gameState.playerHand[originalCardPosition.index] = null;
} else if (originalCardPosition.area === 'player') {
// This is already null from game.down, so this is just for safety.
gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = null;
}
// Remove card graphic from scene and memory
if (selectedCard.parent) {
selectedCard.parent.removeChild(selectedCard);
}
createFloatingText('+' + formatNumberWithSuffix(chipRefund) + ' Chips', selectedCard.x, selectedCard.y, 0xffd700);
selectedCard = null;
originalCardPosition = null;
updateUI();
// Recalculate bonuses if a card was removed from play area
if (originalCardPosition && originalCardPosition.area === 'player') {
applyHandBonuses();
}
return; // Exit early
}
if (targetSlot.area === 'player') {
var existingCard = gameState.playerPlayArea[targetSlot.row][targetSlot.col];
if (existingCard && selectedCard.canMergeWith(existingCard)) {
// Merge in play area
var oldLevel = existingCard.level;
var mergedCard = selectedCard.mergeWith(existingCard);
if (mergedCard) {
gameLayer.removeChild(existingCard);
var handIndex = gameState.playerHand.indexOf(selectedCard);
if (handIndex !== -1) {
uiLayer.removeChild(selectedCard);
gameState.playerHand[handIndex] = null;
} else {
gameLayer.removeChild(selectedCard);
}
var pos = getSlotPosition(targetSlot.row, targetSlot.col, true);
mergedCard.activate(pos.x, pos.y, true);
gameLayer.addChild(mergedCard);
gameState.playerPlayArea[targetSlot.row][targetSlot.col] = mergedCard;
mergedCard.alpha = 0;
mergedCard.scaleX = mergedCard.scaleY = 2;
tween(mergedCard, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.elasticOut
});
LK.getSound('levelUp').play();
createFloatingText('+Level Up!', mergedCard.x, mergedCard.y - 50, 0x00ff00, 60);
var damageBefore = Math.floor(10 * Math.pow(1.6, oldLevel - 1));
var fireRateBefore = Math.max(15, Math.floor(60 / Math.pow(1.2, oldLevel - 1)));
var damageAfter = Math.floor(10 * Math.pow(1.6, mergedCard.level - 1));
var fireRateAfter = Math.max(15, Math.floor(60 / Math.pow(1.2, mergedCard.level - 1)));
var damageIncrease = damageBefore > 0 ? Math.round((damageAfter / damageBefore - 1) * 100) : 0;
var fireRateIncrease = fireRateAfter > 0 && fireRateAfter < fireRateBefore ? Math.round((fireRateBefore / fireRateAfter - 1) * 100) : 0;
var statText = "";
if (damageBefore > 0) {
var dmgMult = (damageAfter / damageBefore).toFixed(2);
statText = "Dmg x" + dmgMult;
}
createFloatingText(statText, mergedCard.x, mergedCard.y + 55, 0x00ff00, 35);
}
} else if (!existingCard) {
// Prevent Jokers from being played on the board directly. They can only merge.
if (selectedCard.cardData.suit === 'joker') {
createFloatingText('Jokers can only level up other cards.', selectedCard.x, selectedCard.y, 0xffcc00);
returnCardToOriginalPosition();
} else {
// Place in empty slot
var pos = getSlotPosition(targetSlot.row, targetSlot.col, true);
selectedCard.activate(pos.x, pos.y, true);
selectedCard.resetInvestment();
var handIndex = gameState.playerHand.indexOf(selectedCard);
if (handIndex !== -1) {
uiLayer.removeChild(selectedCard);
gameLayer.addChild(selectedCard);
gameState.playerHand[handIndex] = null;
}
gameState.playerPlayArea[targetSlot.row][targetSlot.col] = selectedCard;
}
} else {
// Prevent Jokers from being swapped onto the board.
if (selectedCard.cardData.suit === 'joker') {
createFloatingText('Jokers can only level up other cards.', selectedCard.x, selectedCard.y, 0xffcc00);
returnCardToOriginalPosition();
} else {
// Card exists, but cannot merge: swap them with animation
var swappedCard = gameState.playerPlayArea[targetSlot.row][targetSlot.col];
// Determine target positions for animation
var pos1 = getSlotPosition(targetSlot.row, targetSlot.col, true);
var pos2;
tween(selectedCard, {
x: pos1.x,
y: pos1.y
}, {
duration: 200,
easing: tween.quadOut
});
// Update selectedCard state
selectedCard.isInPlay = true;
if (originalCardPosition.area === 'hand') {
selectedCard.resetInvestment(); // Card moved from hand to play area
gameState.playerHand[originalCardPosition.index] = null;
uiLayer.removeChild(selectedCard);
gameLayer.addChild(selectedCard);
}
gameState.playerPlayArea[targetSlot.row][targetSlot.col] = selectedCard;
// Determine target for swappedCard and animate
if (originalCardPosition.area === 'player') {
pos2 = getSlotPosition(originalCardPosition.row, originalCardPosition.col, true);
swappedCard.isInPlay = true;
gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = swappedCard;
} else {
// original was hand, so swapped card goes to hand
var origHandIndex = originalCardPosition.index;
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30;
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
pos2 = {
x: handStartX + origHandIndex * DEAL_SLOT_WIDTH + origHandIndex * 30 + DEAL_SLOT_WIDTH / 2,
y: PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2
};
swappedCard.isInPlay = false;
gameLayer.removeChild(swappedCard);
uiLayer.addChild(swappedCard);
gameState.playerHand[origHandIndex] = swappedCard;
}
tween(swappedCard, {
x: pos2.x,
y: pos2.y
}, {
duration: 200,
easing: tween.quadOut
});
selectedCard.calculateStats();
swappedCard.calculateStats();
}
}
} else if (targetSlot.area === 'hand') {
var existingCard = gameState.playerHand[targetSlot.index];
if (existingCard && existingCard !== selectedCard && selectedCard.canMergeWith(existingCard)) {
// Merge in hand
var oldLevel = existingCard.level;
var mergedCard = selectedCard.mergeWith(existingCard);
if (mergedCard) {
// Remove old cards
var handIndex1 = gameState.playerHand.indexOf(selectedCard);
if (handIndex1 !== -1) {
uiLayer.removeChild(selectedCard);
gameState.playerHand[handIndex1] = null;
} else {
gameLayer.removeChild(selectedCard);
}
var handIndex2 = gameState.playerHand.indexOf(existingCard);
if (handIndex2 !== -1) {
uiLayer.removeChild(existingCard);
gameState.playerHand[handIndex2] = null;
}
// Place merged card in hand
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
var slotX = handStartX + targetSlot.index * DEAL_SLOT_WIDTH + targetSlot.index * 30 + DEAL_SLOT_WIDTH / 2;
var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2;
mergedCard.activate(slotX, slotY, false, true);
uiLayer.addChild(mergedCard);
gameState.playerHand[targetSlot.index] = mergedCard;
// Merge animation
mergedCard.alpha = 0;
mergedCard.scaleX = mergedCard.scaleY = 2;
tween(mergedCard, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.elasticOut
});
LK.getSound('levelUp').play();
createFloatingText('+Level Up!', mergedCard.x, mergedCard.y - 50, 0x00ff00, 60);
var damageBefore = Math.floor(10 * Math.pow(1.6, oldLevel - 1));
var fireRateBefore = Math.max(15, Math.floor(60 / Math.pow(1.2, oldLevel - 1)));
var damageAfter = Math.floor(10 * Math.pow(1.6, mergedCard.level - 1));
var fireRateAfter = Math.max(15, Math.floor(60 / Math.pow(1.2, mergedCard.level - 1)));
var damageIncrease = damageBefore > 0 ? Math.round((damageAfter / damageBefore - 1) * 100) : 0;
var fireRateIncrease = fireRateAfter > 0 && fireRateAfter < fireRateBefore ? Math.round((fireRateBefore / fireRateAfter - 1) * 100) : 0;
var statText = "";
if (damageBefore > 0) {
var dmgMult = (damageAfter / damageBefore).toFixed(2);
statText = "Dmg x" + dmgMult;
}
createFloatingText(statText, mergedCard.x, mergedCard.y + 55, 0x00ff00, 35);
} else {
returnCardToOriginalPosition();
}
} else if (existingCard && existingCard !== selectedCard) {
// Cannot merge, so swap with animation
var swappedCard = gameState.playerHand[targetSlot.index];
// Get target positions
var targetHandIndex = targetSlot.index;
var handWidth1 = 5 * DEAL_SLOT_WIDTH + 4 * 30;
var handStartX1 = (SCREEN_WIDTH - handWidth1) / 2;
var pos1 = {
x: handStartX1 + targetHandIndex * DEAL_SLOT_WIDTH + targetHandIndex * 30 + DEAL_SLOT_WIDTH / 2,
y: PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2
};
var pos2;
// Animate selectedCard
tween(selectedCard, {
x: pos1.x,
y: pos1.y
}, {
duration: 200,
easing: tween.quadOut
});
// Update selectedCard state
selectedCard.isInPlay = false; // It's going to a hand slot
if (originalCardPosition.area === 'player') {
selectedCard.resetInvestment(); // Card moved from play area to hand - should not happen here, but for safety. Investment stops due to isInPlay=false
gameLayer.removeChild(selectedCard);
uiLayer.addChild(selectedCard);
} else {
// This is a hand-to-hand swap, timer should not be reset.
gameState.playerHand[originalCardPosition.index] = null;
}
gameState.playerHand[targetHandIndex] = selectedCard;
// Update swappedCard state
if (originalCardPosition.area === 'player') {
pos2 = getSlotPosition(originalCardPosition.row, originalCardPosition.col, true);
swappedCard.isInPlay = true;
uiLayer.removeChild(swappedCard);
gameLayer.addChild(swappedCard);
gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = swappedCard;
} else {
// original was hand
var origHandIndex = originalCardPosition.index;
var handWidth2 = 5 * DEAL_SLOT_WIDTH + 4 * 30;
var handStartX2 = (SCREEN_WIDTH - handWidth2) / 2;
pos2 = {
x: handStartX2 + origHandIndex * DEAL_SLOT_WIDTH + origHandIndex * 30 + DEAL_SLOT_WIDTH / 2,
y: PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2
};
swappedCard.isInPlay = false;
gameState.playerHand[origHandIndex] = swappedCard;
}
tween(swappedCard, {
x: pos2.x,
y: pos2.y
}, {
duration: 200,
easing: tween.quadOut
});
selectedCard.calculateStats();
swappedCard.calculateStats();
} else {
returnCardToOriginalPosition();
}
}
} else {
returnCardToOriginalPosition();
}
selectedCard = null;
originalCardPosition = null;
applyHandBonuses();
}
};
function returnCardToOriginalPosition() {
if (!selectedCard || !originalCardPosition) {
return;
}
if (originalCardPosition.area === 'hand') {
// Return to hand slot
var handIndex = originalCardPosition.index;
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30; // 5 slots + 4 gaps of 30px
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
var slotX = handStartX + handIndex * DEAL_SLOT_WIDTH + handIndex * 30 + DEAL_SLOT_WIDTH / 2;
var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2;
selectedCard.x = slotX;
selectedCard.y = slotY;
gameState.playerHand[handIndex] = selectedCard;
} else if (originalCardPosition.area === 'player') {
// Return to play area slot
var pos = getSlotPosition(originalCardPosition.row, originalCardPosition.col, true);
selectedCard.x = pos.x;
selectedCard.y = pos.y;
gameState.playerPlayArea[originalCardPosition.row][originalCardPosition.col] = selectedCard;
}
}
function createOutlinedText(text, options, outlineColor, outlineThickness) {
var container = new Container();
// Create the outline by placing shadow texts in 8 directions
var offsets = [[-outlineThickness, -outlineThickness],
// top-left
[0, -outlineThickness],
// top
[outlineThickness, -outlineThickness],
// top-right
[outlineThickness, 0],
// right
[outlineThickness, outlineThickness],
// bottom-right
[0, outlineThickness],
// bottom
[-outlineThickness, outlineThickness],
// bottom-left
[-outlineThickness, 0] // left
];
// Create outline shadows
offsets.forEach(function (offset) {
var shadowText = new Text2(text, {
size: options.size,
fill: outlineColor,
weight: options.weight
// Don't use stroke properties for shadows
});
shadowText.anchor.set(options.anchor ? options.anchor[0] : 0.5, options.anchor ? options.anchor[1] : 0.5);
shadowText.x = offset[0];
shadowText.y = offset[1];
container.addChild(shadowText);
});
// Create the main text on top
var mainText = new Text2(text, {
size: options.size,
fill: options.fill,
weight: options.weight
});
mainText.anchor.set(options.anchor ? options.anchor[0] : 0.5, options.anchor ? options.anchor[1] : 0.5);
container.addChild(mainText);
// Add a setText method to the container for easy updates
container.setText = function (newText) {
// Update all shadow texts
for (var i = 0; i < offsets.length; i++) {
container.children[i].setText(newText);
}
// Update main text
mainText.setText(newText);
};
return container;
}
function createFloatingText(text, x, y, color, size) {
var textOptions = {
size: size || 40,
fill: color || 0xffffff,
weight: 800,
anchor: [0.5, 0.5]
};
var outlineColor = 0x000000;
var outlineThickness = 3;
// Add black outline for green level up text
if (color === 0x00ff00) {
outlineThickness = 6;
textOptions.size = (size || 40) * 1.5; // Make it smaller
}
// Make poker damage text white with black outline, bigger, and animate faster
if (color === 0xff0000 && text.startsWith('-')) {
textOptions.fill = 0xffffff; // White color
outlineColor = 0x000000; // Black outline
outlineThickness = 6; // Black outline thickness
textOptions.size = (size || 40) * 1.5; // Make it 50% bigger (increased from 30%)
}
var floatingText = createOutlinedText(text, textOptions, outlineColor, outlineThickness);
floatingText.x = x;
floatingText.y = y;
floatingText.alpha = 1;
uiLayer.addChild(floatingText);
// Use larger animation for important messages
var animationDistance = size && size > 40 ? 120 : 80;
var animationDuration = size && size > 40 ? 2000 : 1500;
// Make poker damage text animate faster
if (color === 0xff0000 && text.startsWith('-')) {
animationDuration = 800; // Much faster animation
}
// Level up text fades faster now
if (color === 0x00ff00) {
animationDuration = 1000;
}
// Split animation into two parts
var floatDuration = animationDuration * 0.6; // 60% of time for floating up
var fadeDuration = animationDuration * 0.4; // 40% of time for fading out
// First part: Float upwards with no alpha change
tween(floatingText, {
y: y - animationDistance * 0.7 // Float up 70% of the distance
}, {
duration: floatDuration,
easing: tween.quadOut,
onFinish: function onFinish() {
// Second part: Continue floating while fading out
tween(floatingText, {
y: y - animationDistance,
alpha: 0
}, {
duration: fadeDuration,
easing: tween.quadOut,
onFinish: function onFinish() {
uiLayer.removeChild(floatingText);
}
});
}
});
}
function createExplosion(x, y, color) {
var numParticles = 5;
for (var i = 0; i < numParticles; i++) {
var particle = LK.getAsset('explosionParticle', {
anchorX: 0.5,
anchorY: 0.5
});
particle.x = x;
particle.y = y;
particle.tint = color;
particle.scale.set(0.5 + Math.random() * 0.5);
var angle = Math.random() * Math.PI * 2;
var distance = Math.random() * 50 + 20;
var duration = 500 + Math.random() * 500;
var targetX = x + Math.cos(angle) * distance;
var targetY = y + Math.sin(angle) * distance;
gameLayer.addChild(particle);
tween(particle, {
x: targetX,
y: targetY,
alpha: 0
}, {
duration: duration,
easing: tween.quadOut,
onFinish: function (p) {
return function () {
if (p.parent) {
p.parent.removeChild(p);
}
};
}(particle)
});
}
}
/****
* Main Game Loop
****/
game.update = function () {
// Update tutorial system
if (TutorialSystem.isActive) {
TutorialSystem.update();
}
// Only run game logic when in playing state
if (currentGameState !== 'playing') {
if (currentGameState === 'start' && backgroundSuits.length > 0) {
var speed = 0.5;
var spacing = 400;
// Calculate pattern dimensions
var numCols = Math.ceil(SCREEN_WIDTH / spacing) + 3;
var numRows = Math.ceil(SCREEN_HEIGHT / spacing) + 3;
var patternWidth = numCols * spacing;
var patternHeight = numRows * spacing;
for (var i = 0; i < backgroundSuits.length; i++) {
var suit = backgroundSuits[i];
// Move diagonally
suit.x += speed;
suit.y += speed;
// Wrap horizontally
if (suit.x > SCREEN_WIDTH + spacing * 1.5) {
suit.x -= patternWidth;
}
// Wrap vertically
if (suit.y > SCREEN_HEIGHT + spacing * 1.5) {
suit.y -= patternHeight;
}
}
}
return;
}
if (!gameReady) {
return;
}
// Update wave spawning system
WaveSystem.update();
// Update active chips
for (var i = activePlayerChips.length - 1; i >= 0; i--) {
activePlayerChips[i].update();
}
for (var i = activeAIChips.length - 1; i >= 0; i--) {
activeAIChips[i].update();
}
// Tutorial Assistance: If an enemy on the player's side gets halfway, destroy it.
if (TutorialSystem.isActive) {
var assistancePathThreshold = 50; // Halfway down the path
for (var i = activePlayerChips.length - 1; i >= 0; i--) {
var chip = activePlayerChips[i];
if (chip.pathProgress > assistancePathThreshold) {
if (!TutorialSystem.assistanceMessageShown) {
TutorialSystem.assistanceMessageShown = true;
var message = "It looks like you could use some help! I've taken care of that enemy for you.\n\nThis helping hand is only available in the tutorial. Try adding more cards, merging them, and aiming for better poker hands to get stronger!";
TutorialSystem.showMessage(message, null, function () {
TutorialSystem.clearMessage();
});
}
// Instantly destroy the chip to assist the player, preventing them from losing in the tutorial.
chip.takeDamage(chip.health * 2);
}
}
}
// Update active mines
for (var i = activeMines.length - 1; i >= 0; i--) {
if (activeMines[i]) {
activeMines[i].update();
}
}
// Update active bullets
for (var i = activeBullets.length - 1; i >= 0; i--) {
activeBullets[i].update();
}
// Update cards in play
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
if (gameState.playerPlayArea[row][col]) {
gameState.playerPlayArea[row][col].update();
}
if (gameState.aiPlayArea[row][col]) {
gameState.aiPlayArea[row][col].update();
}
}
}
// Update AI
AISystem.update();
// Check win/lose conditions
// Update life displays
while (gameState.playerLives < lastPlayerLives) {
lastPlayerLives--;
var heartToFade = playerLifeHearts[lastPlayerLives];
if (heartToFade) {
tween(heartToFade, {
alpha: 0.2
}, {
duration: 500
});
}
}
while (gameState.aiLives < lastAiLives) {
lastAiLives--;
var heartToFade = aiLifeHearts[lastAiLives];
if (heartToFade) {
tween(heartToFade, {
alpha: 0.2
}, {
duration: 500
});
}
}
if (gameMode === 'coop') {
if (gameState.playerLives <= 0) {
showCoopResultsScreen();
}
} else {
if (gameState.playerLives <= 0) {
showGameOver(false);
} else if (gameState.aiLives <= 0) {
showGameOver(true);
}
}
updateUI();
};
/****
* Game Over
****/
function resetGameAndReturnToMenu() {
// This function now encapsulates all the logic to reset the game to its initial state.
// 1. Remove all dynamically created game objects from the stage.
[].concat(_toConsumableArray(activePlayerChips), _toConsumableArray(activeAIChips), _toConsumableArray(activeBullets), _toConsumableArray(activeMines)).forEach(function (obj) {
if (obj.parent) {
obj.parent.removeChild(obj);
}
});
for (var r = 0; r < PLAY_AREA_ROWS; r++) {
for (var c = 0; c < PLAY_AREA_COLS; c++) {
var pCard = gameState.playerPlayArea[r][c];
if (pCard && pCard.parent) {
pCard.parent.removeChild(pCard);
}
var aCard = gameState.aiPlayArea[r][c];
if (aCard && aCard.parent) {
aCard.parent.removeChild(aCard);
}
}
}
gameState.playerHand.forEach(function (card) {
if (card && card.parent) {
card.parent.removeChild(card);
}
});
playerLifeHearts.forEach(function (h) {
if (h.parent) {
h.parent.removeChild(h);
}
});
aiLifeHearts.forEach(function (h) {
if (h.parent) {
h.parent.removeChild(h);
}
});
playerHandNameTexts.forEach(function (t) {
if (t && t.parent) {
t.parent.removeChild(t);
}
});
if (opponentNameText && opponentNameText.parent) {
opponentNameText.parent.removeChild(opponentNameText);
}
if (playerNameText && playerNameText.parent) {
playerNameText.parent.removeChild(playerNameText);
}
slotIndicators.forEach(function (indicator) {
if (indicator.parent) {
indicator.parent.removeChild(indicator);
}
});
slotIndicators = [];
// 2. Clear all tracking arrays.
activePlayerChips = [];
activeAIChips = [];
activeBullets = [];
activeMines = [];
playerLifeHearts = [];
aiLifeHearts = [];
playerHandNameTexts = [];
opponentNameText = null;
playerNameText = null;
// 3. Reset game state variables.
gameState.playerChips = 200;
gameState.aiChips = 200;
gameState.playerLives = 3;
gameState.aiLives = 3;
gameState.isPlayerTurn = true;
gameState.dealCost = 25; // Reset to starting cost of 25
gameState.dealCount = 0; // Reset to starting count
gameState.playerDeck = [];
gameState.playerHand = [];
gameState.playerPlayArea = [];
gameState.aiDeck = [];
gameState.aiPlayArea = [];
// 4. Reset game systems.
WaveSystem.waveNumber = 1;
WaveSystem.waveTimer = 0;
WaveSystem.bossSpawned = false;
WaveSystem.playerBossDefeated = false;
WaveSystem.aiBossDefeated = false;
AISystem.thinkTimer = 0;
// 5. Reset other global variables.
lastPlayerLives = 0;
lastAiLives = 0;
selectedCard = null;
isDragging = false;
originalCardPosition = null;
// 6. Hide persistent UI.
gameElements.forEach(function (el) {
el.visible = false;
});
background.visible = false;
coopBackground.visible = false;
// 7. Return to the main menu.
createStartScreen(true);
}
function showResultsScreen() {
var resultsContainer = new Container();
uiLayer.addChild(resultsContainer);
// Dark overlay
var bg = LK.getAsset('card', {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
anchorX: 0,
anchorY: 0
});
bg.tint = 0x000000;
bg.alpha = 0.7;
resultsContainer.addChild(bg);
var title = new Text2('VICTORY!', {
size: 180,
fill: 0xffd700,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 10
});
title.anchor.set(0.5, 0.5);
title.x = SCREEN_WIDTH / 2;
title.y = 300;
resultsContainer.addChild(title);
// Calculate Battle Rating
var oldRating = storage.battleRating;
var waveBonus = (WaveSystem.waveNumber - 1) * 5;
var livesBonus = (gameState.playerLives - gameState.aiLives) * 10;
var winBonus = 25;
var ratingChange = waveBonus + livesBonus + winBonus;
ratingChange = Math.max(5, ratingChange); // Minimum 5 rating gain on win
var newRating = oldRating + ratingChange;
storage.battleRating = newRating;
// Display Rating Calculation
var yPos = 600;
var yStep = 120;
var ySubStep = 90;
var oldRatingText = new Text2('Previous Rating: ' + oldRating, {
size: 80,
fill: 0xffffff,
weight: 'bold'
});
oldRatingText.anchor.set(0.5, 0.5);
oldRatingText.x = SCREEN_WIDTH / 2;
oldRatingText.y = yPos;
resultsContainer.addChild(oldRatingText);
yPos += yStep;
var waveBonusText = new Text2('Wave Bonus (Wave ' + WaveSystem.waveNumber + '): +' + waveBonus, {
size: 70,
fill: 0x88ff88,
weight: 'normal'
});
waveBonusText.anchor.set(0.5, 0.5);
waveBonusText.x = SCREEN_WIDTH / 2;
waveBonusText.y = yPos;
resultsContainer.addChild(waveBonusText);
yPos += ySubStep;
var livesBonusText;
if (livesBonus >= 0) {
livesBonusText = new Text2('Performance Bonus: +' + livesBonus, {
size: 70,
fill: 0x88ff88,
weight: 'normal'
});
} else {
livesBonusText = new Text2('Performance Bonus: ' + livesBonus, {
size: 70,
fill: 0xff8888,
weight: 'normal'
});
}
livesBonusText.anchor.set(0.5, 0.5);
livesBonusText.x = SCREEN_WIDTH / 2;
livesBonusText.y = yPos;
resultsContainer.addChild(livesBonusText);
yPos += ySubStep;
var winBonusText = new Text2('Victory Bonus: +' + winBonus, {
size: 70,
fill: 0x88ff88,
weight: 'normal'
});
winBonusText.anchor.set(0.5, 0.5);
winBonusText.x = SCREEN_WIDTH / 2;
winBonusText.y = yPos;
resultsContainer.addChild(winBonusText);
yPos += yStep + 20;
var lineBreak = LK.getAsset('lineBreak', {
anchorX: 0.5,
anchorY: 0.5
});
lineBreak.x = SCREEN_WIDTH / 2;
lineBreak.y = yPos;
lineBreak.scale.set(0.8);
resultsContainer.addChild(lineBreak);
yPos += yStep;
var newRatingText = new Text2('New Rating: ' + newRating, {
size: 100,
fill: 0xffffff,
weight: 'bold'
});
newRatingText.anchor.set(0.5, 0.5);
var battleRatingIcon = LK.getAsset('battleRatingIcon', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
var totalWidth = battleRatingIcon.width + 15 + newRatingText.width;
battleRatingIcon.x = SCREEN_WIDTH / 2 - totalWidth / 2 + battleRatingIcon.width / 2;
battleRatingIcon.y = yPos;
newRatingText.x = battleRatingIcon.x + battleRatingIcon.width / 2 + 15 + newRatingText.width / 2;
newRatingText.y = yPos;
resultsContainer.addChild(battleRatingIcon);
resultsContainer.addChild(newRatingText);
// Create chest container
var chestContainer = new Container();
chestContainer.x = SCREEN_WIDTH / 2;
chestContainer.y = 1600;
resultsContainer.addChild(chestContainer);
// Add small chest
var chest = LK.getAsset('smallChest', {
anchorX: 0.5,
anchorY: 0.5
});
chest.scale.set(0.01);
chestContainer.addChild(chest);
// Tap to open text
var tapToOpenText = new Text2('Tap to open', {
size: 60,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 4
});
tapToOpenText.anchor.set(0.5, 0.5);
tapToOpenText.y = 120;
tapToOpenText.visible = false;
chestContainer.addChild(tapToOpenText);
// Make chest interactive
chestContainer.interactive = true;
chestContainer.hitArea = new Rectangle(-150, -150, 300, 300);
var chestOpened = false;
var moneyReward = 0;
// Animate chest in with bounce
tween(chest, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 800,
easing: tween.elasticOut,
onFinish: function onFinish() {
tapToOpenText.visible = true;
// Pulse animation for tap to open text
function pulseText() {
if (!tapToOpenText.parent || chestOpened) {
return;
}
tween(tapToOpenText, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!tapToOpenText.parent || chestOpened) {
return;
}
tween(tapToOpenText, {
scaleX: 1,
scaleY: 1
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: pulseText
});
}
});
}
pulseText();
}
});
chestContainer.down = function () {
if (chestOpened) {
return;
}
chestOpened = true;
tapToOpenText.visible = false;
// Wiggle animation
tween(chest, {
rotation: -0.1
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
tween(chest, {
rotation: 0.1
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
tween(chest, {
rotation: 0
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
// Flash screen white
LK.effects.flashScreen(0xffffff, 300);
// Replace with open chest
chestContainer.removeChild(chest);
var openChest = LK.getAsset('smallChestOpen', {
anchorX: 0.5,
anchorY: 0.5
});
openChest.scale.set(1.5);
chestContainer.addChild(openChest);
// Generate random money reward
moneyReward = Math.floor(Math.random() * 151) + 100; // 100-250
var startMoney = storage.money;
var targetMoney = startMoney + moneyReward;
// Money text animation
var moneyText = new Text2('$' + moneyReward, {
size: 150,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 8
});
moneyText.anchor.set(0.5, 0.5);
moneyText.y = -50;
moneyText.scale.set(0.1);
moneyText.alpha = 0;
chestContainer.addChild(moneyText);
// Animate scale property directly to avoid potential proxy issues with Text2 objects
tween(moneyText.scale, {
x: 1,
y: 1
}, {
duration: 500,
easing: tween.backOut
});
// Animate other properties on the text object itself
tween(moneyText, {
alpha: 1,
y: -100
}, {
duration: 500,
easing: tween.backOut,
onFinish: function onFinish() {
// Rolling number animation
var rollDuration = 1000;
var startTime = Date.now();
var rollInterval = LK.setInterval(function () {
var elapsed = Date.now() - startTime;
var progress = Math.min(elapsed / rollDuration, 1);
var easedProgress = 1 - Math.pow(1 - progress, 3); // Cubic ease out
var currentValue = Math.floor(startMoney + moneyReward * easedProgress);
storage.money = currentValue;
if (progress >= 1) {
LK.clearInterval(rollInterval);
storage.money = targetMoney;
// Show tap to continue
var tapToContinueText = new Text2('Tap to continue', {
size: 70,
fill: 0x000000,
weight: 'bold'
});
tapToContinueText.anchor.set(0.5, 0.5);
tapToContinueText.y = 200;
chestContainer.addChild(tapToContinueText);
// Update chest interaction
chestContainer.down = function () {
tween(resultsContainer, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
if (resultsContainer.parent) {
resultsContainer.parent.removeChild(resultsContainer);
}
resetGameAndReturnToMenu();
}
});
};
}
}, 16); // ~60fps
}
});
}
});
}
});
}
});
};
// Animate the screen in
resultsContainer.alpha = 0;
tween(resultsContainer, {
alpha: 1
}, {
duration: 500
});
}
function showGameOver(playerWon) {
// Stop all game action by changing the state. The main update loop will halt.
LK.stopMusic();
currentGameState = 'gameover_transition';
// NEW: Immediately stop all active game objects and create particle effects
stopAllActionWithParticles();
// Animate the victory or defeat logo.
var logoAssetId = playerWon ? 'victoryLogo' : 'defeatLogo';
var endLogo = LK.getAsset(logoAssetId, {
anchorX: 0.5,
anchorY: 0.5,
x: SCREEN_WIDTH / 2,
y: SCREEN_HEIGHT / 2
});
endLogo.scale.set(0.01);
uiLayer.addChild(endLogo);
tween(endLogo, {
scaleX: 1,
scaleY: 1
}, {
duration: 1200,
easing: tween.elasticOut,
onFinish: function onFinish() {
// After the animation, wait a moment, then transition.
LK.setTimeout(function () {
if (endLogo.parent) {
endLogo.parent.removeChild(endLogo);
}
if (playerWon) {
if (TutorialSystem.isActive) {
// In tutorial, we skip the normal results and go straight to reset/menu to continue tutorial
resetGameAndReturnToMenu();
} else {
showResultsScreen();
}
} else {
if (TutorialSystem.isActive) {
// Player lost tutorial, let them retry from beginning
storage.tutorialCompleted = false;
}
resetGameAndReturnToMenu();
}
}, 1500);
}
});
}
function showCoopResultsScreen() {
LK.stopMusic();
currentGameState = 'gameover_transition';
// NEW: Immediately stop all active game objects and create particle effects
stopAllActionWithParticles();
var resultsContainer = new Container();
uiLayer.addChild(resultsContainer);
// ... rest of the function remains the same
// Dark overlay
var bg = LK.getAsset('card', {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
anchorX: 0,
anchorY: 0
});
bg.tint = 0x000000;
bg.alpha = 0.7;
resultsContainer.addChild(bg);
var title = new Text2('RESULTS', {
size: 180,
fill: 0xffd700,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 10
});
title.anchor.set(0.5, 0.5);
title.x = SCREEN_WIDTH / 2;
title.y = 300;
resultsContainer.addChild(title);
// Wave Reached
var waveText = new Text2('Wave Reached: ' + WaveSystem.waveNumber, {
size: 80,
fill: 0xffffff,
weight: 'bold'
});
waveText.anchor.set(0.5, 0.5);
waveText.x = SCREEN_WIDTH / 2;
waveText.y = 600;
resultsContainer.addChild(waveText);
if (WaveSystem.waveNumber > (storage.coopHighestWave || 0)) {
storage.coopHighestWave = WaveSystem.waveNumber;
}
// Damage percentages
var totalDamage = playerTotalDamage + aiTotalDamage;
var playerPercent = totalDamage > 0 ? Math.round(playerTotalDamage / totalDamage * 100) : 0;
var aiPercent = totalDamage > 0 ? 100 - playerPercent : 0;
var playerDamageText = new Text2('Player Damage: ' + playerPercent + '%', {
size: 70,
fill: 0x88ff88,
weight: 'normal'
});
playerDamageText.anchor.set(0.5, 0.5);
playerDamageText.x = SCREEN_WIDTH / 2;
playerDamageText.y = 750;
resultsContainer.addChild(playerDamageText);
var aiDamageText = new Text2('AI Damage: ' + aiPercent + '%', {
size: 70,
fill: 0xff8888,
weight: 'normal'
});
aiDamageText.anchor.set(0.5, 0.5);
aiDamageText.x = SCREEN_WIDTH / 2;
aiDamageText.y = 850;
resultsContainer.addChild(aiDamageText);
// Create chest container
var chestContainer = new Container();
chestContainer.x = SCREEN_WIDTH / 2;
chestContainer.y = 1600;
resultsContainer.addChild(chestContainer);
// Add small chest
var chest = LK.getAsset('smallChest', {
anchorX: 0.5,
anchorY: 0.5
});
chest.scale.set(0.01);
chestContainer.addChild(chest);
// Tap to open text
var tapToOpenText = new Text2('Tap to open', {
size: 60,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 4
});
tapToOpenText.anchor.set(0.5, 0.5);
tapToOpenText.y = 120;
tapToOpenText.visible = false;
chestContainer.addChild(tapToOpenText);
// Make chest interactive
chestContainer.interactive = true;
chestContainer.hitArea = new Rectangle(-150, -150, 300, 300);
var chestOpened = false;
// Animate chest in with bounce
tween(chest, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 800,
easing: tween.elasticOut,
onFinish: function onFinish() {
tapToOpenText.visible = true;
// Pulse animation for tap to open text
function pulseText() {
if (!tapToOpenText.parent || chestOpened) {
return;
}
tween(tapToOpenText, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!tapToOpenText.parent || chestOpened) {
return;
}
tween(tapToOpenText, {
scaleX: 1,
scaleY: 1
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: pulseText
});
}
});
}
pulseText();
}
});
chestContainer.down = function () {
if (chestOpened) {
return;
}
chestOpened = true;
tapToOpenText.visible = false;
// Wiggle animation
tween(chest, {
rotation: -0.1
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
tween(chest, {
rotation: 0.1
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
tween(chest, {
rotation: 0
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
// Flash screen white
LK.effects.flashScreen(0xffffff, 300);
// Replace with open chest
chestContainer.removeChild(chest);
var openChest = LK.getAsset('smallChestOpen', {
anchorX: 0.5,
anchorY: 0.5
});
openChest.scale.set(1.5);
chestContainer.addChild(openChest);
// Reward calculation
var moneyReward = Math.floor(WaveSystem.waveNumber * 5 + playerPercent);
var startMoney = storage.money;
var targetMoney = startMoney + moneyReward;
// Money text animation
var moneyText = new Text2('$' + moneyReward, {
size: 150,
fill: 0xffd700,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 8
});
moneyText.anchor.set(0.5, 0.5);
moneyText.y = -50;
moneyText.scale.set(0.1);
moneyText.alpha = 0;
chestContainer.addChild(moneyText);
// Animate scale property directly to avoid potential proxy issues with Text2 objects
tween(moneyText.scale, {
x: 1,
y: 1
}, {
duration: 500,
easing: tween.backOut
});
// Animate other properties on the text object itself
tween(moneyText, {
alpha: 1,
y: -100
}, {
duration: 500,
easing: tween.backOut,
onFinish: function onFinish() {
// Rolling number animation
var rollDuration = 1000;
var startTime = Date.now();
var rollInterval = LK.setInterval(function () {
var elapsed = Date.now() - startTime;
var progress = Math.min(elapsed / rollDuration, 1);
var easedProgress = 1 - Math.pow(1 - progress, 3); // Cubic ease out
var currentValue = Math.floor(startMoney + moneyReward * easedProgress);
storage.money = currentValue;
if (progress >= 1) {
LK.clearInterval(rollInterval);
storage.money = targetMoney;
// Show tap to continue
var tapToContinueText = new Text2('Tap to continue', {
size: 70,
fill: 0x000000,
weight: 'bold'
});
tapToContinueText.anchor.set(0.5, 0.5);
tapToContinueText.y = 200;
chestContainer.addChild(tapToContinueText);
// Update chest interaction
chestContainer.down = function () {
tween(resultsContainer, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
if (resultsContainer.parent) {
resultsContainer.parent.removeChild(resultsContainer);
}
resetGameAndReturnToMenu();
}
});
};
}
}, 16); // ~60fps
}
});
}
});
}
});
}
});
};
// Animate the screen in
resultsContainer.alpha = 0;
tween(resultsContainer, {
alpha: 1
}, {
duration: 500
});
}
// NEW: Function to stop all action and create particle effects
function stopAllActionWithParticles() {
// Create explosion particles for all remaining chips
[].concat(activePlayerChips, activeAIChips).forEach(function (chip) {
if (chip.active && chip.visible) {
createChipExplosionParticles(chip.x, chip.y, chip.value);
}
chip.active = false;
chip.visible = false;
if (chip.parent) {
chip.parent.removeChild(chip);
}
});
// Create explosion particles for bullets
activeBullets.forEach(function (bullet) {
if (bullet.active && bullet.visible) {
var explosionColor = 0x333333; // Dark Grey for clubs/spades
if (bullet.suit === 'hearts' || bullet.suit === 'diamonds') {
explosionColor = 0xff0000; // Red for hearts/diamonds
}
createExplosion(bullet.x, bullet.y, explosionColor);
}
bullet.active = false;
bullet.visible = false;
if (bullet.parent) {
bullet.parent.removeChild(bullet);
}
});
// Explode mines
activeMines.forEach(function (mine) {
if (mine.active && mine.visible) {
createExplosion(mine.x, mine.y, 0xffa500);
}
mine.active = false;
mine.visible = false;
if (mine.parent) {
mine.parent.removeChild(mine);
}
});
// Clear the arrays
activePlayerChips.length = 0;
activeAIChips.length = 0;
activeBullets.length = 0;
activeMines.length = 0;
// Stop all card firing by marking them as not in play temporarily
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
for (var col = 0; col < PLAY_AREA_COLS; col++) {
if (gameState.playerPlayArea[row][col]) {
gameState.playerPlayArea[row][col].isInPlay = false;
}
if (gameState.aiPlayArea[row][col]) {
gameState.aiPlayArea[row][col].isInPlay = false;
}
}
}
}
// NEW: Enhanced particle effect for chip explosions - celebratory version
function createChipExplosionParticles(x, y, chipValue) {
var numParticles = Math.min(20, 10 + chipValue); // More particles for celebration
var chipColor = getChipColor(chipValue);
for (var i = 0; i < numParticles; i++) {
var particle = LK.getAsset('explosionParticle', {
anchorX: 0.5,
anchorY: 0.5
});
particle.x = x + (Math.random() - 0.5) * 40; // Start in a tight cluster
particle.y = y + (Math.random() - 0.5) * 40;
particle.tint = chipColor;
particle.scale.set(0.4 + Math.random() * 0.8);
// Launch particles upward in a celebratory fountain pattern
var angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 0.8; // Mostly upward, some spread
var initialSpeed = 300 + Math.random() * 400; // Strong upward velocity
var gravity = 600; // Gravity pulls them down
var velocityX = Math.cos(angle) * initialSpeed;
var velocityY = Math.sin(angle) * initialSpeed; // Negative Y is upward
var duration = 2000 + Math.random() * 1000; // 2-3 seconds of flight time
var targetX = x + velocityX * (duration / 1000);
var targetY = y + velocityY * (duration / 1000) + 0.5 * gravity * Math.pow(duration / 1000, 2);
// Ensure particles fall well off screen for full celebration effect
if (targetY < SCREEN_HEIGHT + 200) {
targetY = SCREEN_HEIGHT + 200 + Math.random() * 300;
}
gameLayer.addChild(particle);
// Animate with celebratory spinning and fading
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
rotation: Math.random() * Math.PI * 6 // Extra spinning for celebration
}, {
duration: duration,
easing: tween.quadIn,
// Natural gravity acceleration
onFinish: function (p) {
return function () {
if (p.parent) {
p.parent.removeChild(p);
}
};
}(particle)
});
}
}
// NEW: Helper function to get chip color based on value
function getChipColor(chipValue) {
switch (chipValue) {
case 1:
return 0xffff00;
// Yellow
case 5:
return 0xff0000;
// Red
case 10:
return 0x00ff00;
// Green
case 25:
return 0x0000ff;
// Blue
case 100:
return 0x8000ff;
// Purple
default:
return 0xffffff;
// White for boss chips
}
}
/****
* Utility Functions
****/
function formatNumberWithSuffix(number) {
var num = Math.round(number);
if (num < 1000) {
return num.toString();
}
if (num < 1000000) {
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
}
if (num < 1000000000) {
return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
}
if (num < 1000000000000) {
return (num / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B';
}
return (num / 1000000000000).toFixed(1).replace(/\.0$/, '') + 'T';
}
function clearPlayerStorage() {
storage.battleRating = 0;
storage.money = 0;
storage.equippedMods = null;
storage.ownedMods = null;
storage.randomModPrice = 300;
storage.tutorialCompleted = false; // Also reset tutorial progress
storage.coopHighestWave = 0; // Clear highest wave in coop
// Also reset in-memory ModSystem if it's already initialized
ModSystem.equippedMods = {
hearts: null,
diamonds: null,
clubs: null,
spades: null
};
ModSystem.ownedMods = {};
createFloatingText("Player Storage Cleared!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 0xff0000, 100);
}
var TutorialSystem = {
isActive: false,
step: 0,
overlay: null,
messageBox: null,
pulsingElement: null,
isWaitingForTap: false,
isWaitingForDrag: false,
isWaitingForMerge: false,
isWaitingForChipKill: false,
isWaitingForJokerMerge: false,
nextStepCallback: null,
assistanceMessageShown: false,
startScreenItems: {},
registerStartScreenElements: function registerStartScreenElements(items) {
this.startScreenItems = items;
},
start: function start() {
this.isActive = true;
this.step = 0;
this.assistanceMessageShown = false;
},
startInGameTutorial: function startInGameTutorial() {
this.isActive = true;
this.step = 1;
this.assistanceMessageShown = false;
dealInitialHand();
var self = this;
LK.setTimeout(function () {
self.advanceStep();
}, 1500);
},
update: function update() {
if (!this.isActive) {
return;
}
if (this.isWaitingForDrag) {
for (var r = 0; r < PLAY_AREA_ROWS; r++) {
for (var c = 0; c < PLAY_AREA_COLS; c++) {
if (gameState.playerPlayArea[r][c]) {
this.isWaitingForDrag = false;
this.advanceStep();
return;
}
}
}
}
if (this.isWaitingForMerge) {
var mergeCompleted = false;
// Check play area for a leveled-up card
for (var r = 0; r < PLAY_AREA_ROWS; r++) {
for (var c = 0; c < PLAY_AREA_COLS; c++) {
if (gameState.playerPlayArea[r][c] && gameState.playerPlayArea[r][c].level >= 2) {
mergeCompleted = true;
break;
}
}
if (mergeCompleted) {
break;
}
}
// If not found in play area, check the hand
if (!mergeCompleted) {
for (var i = 0; i < gameState.playerHand.length; i++) {
if (gameState.playerHand[i] && gameState.playerHand[i].level >= 2) {
mergeCompleted = true;
break;
}
}
}
if (mergeCompleted) {
this.isWaitingForMerge = false;
this.advanceStep();
return;
}
}
if (this.isWaitingForJokerMerge) {
var hasLevel3PlusCard = false;
var level2PlusCardCount = 0;
// Check play area for cards
for (var r = 0; r < PLAY_AREA_ROWS; r++) {
for (var c = 0; c < PLAY_AREA_COLS; c++) {
var card = gameState.playerPlayArea[r][c];
if (card) {
if (card.level >= 3) {
hasLevel3PlusCard = true;
}
if (card.level >= 2) {
level2PlusCardCount++;
}
}
}
}
// Also check player's hand
for (var i = 0; i < gameState.playerHand.length; i++) {
var card = gameState.playerHand[i];
if (card) {
if (card.level >= 3) {
hasLevel3PlusCard = true;
}
if (card.level >= 2) {
level2PlusCardCount++;
}
}
}
if (hasLevel3PlusCard || level2PlusCardCount >= 2) {
this.isWaitingForJokerMerge = false;
this.advanceStep();
return;
}
}
if (this.isWaitingForChipKill) {
if (activePlayerChips.length === 0) {
this.isWaitingForChipKill = false;
this.advanceStep();
}
}
},
advanceStep: function advanceStep() {
this.clearMessage();
this.step++;
switch (this.step) {
case 2:
this.explainDrag();
break;
case 3:
this.waitForDrag();
break;
case 4:
this.spawnFirstEnemy();
break;
case 5:
this.waitForKill();
break;
case 6:
this.explainMerge();
break;
case 7:
this.waitForMerge();
break;
case 8:
this.explainJoker();
break;
case 9:
this.instructJokerMerge();
break;
case 10:
this.startWaves();
break;
case 11:
this.explainPokerHands();
break;
case 12:
this.explainDiscard();
break;
case 13:
this.guaranteedWin();
break;
case 14:
this.guideToShop();
break;
case 15:
this.guideToRandomMod();
break;
case 16:
this.guideToModsScreen();
break;
case 17:
this.explainEquipMod();
break;
case 18:
this.completeTutorial();
break;
}
},
showMessage: function showMessage(text, elementToPulse, waitForTapCallback, isPostGame) {
this.clearMessage();
this.overlay = new Container();
if (!isPostGame) {
this.overlay.interactive = true;
}
this.overlay.hitArea = new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
var bg = LK.getAsset('card', {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
anchorX: 0,
anchorY: 0
});
bg.tint = 0x000000;
bg.alpha = 0.7;
this.overlay.addChild(bg);
uiLayer.addChild(this.overlay);
this.messageBox = new Text2(text, {
size: 70,
fill: 0xffffff,
weight: 'bold',
stroke: 0x000000,
strokeThickness: 5,
align: 'center',
wordWrap: true,
wordWrapWidth: SCREEN_WIDTH - 200
});
this.messageBox.anchor.set(0.5, 0.5);
this.messageBox.x = SCREEN_WIDTH / 2;
this.messageBox.y = isPostGame ? SCREEN_HEIGHT - 650 : SCREEN_HEIGHT / 2;
this.overlay.addChild(this.messageBox);
if (elementToPulse) {
this.pulsingElement = elementToPulse;
if (elementToPulse.parent) {
this.pulsingElementOriginalParent = elementToPulse.parent;
this.overlay.addChild(elementToPulse);
}
this.pulseAnimation(elementToPulse);
}
if (waitForTapCallback) {
this.isWaitingForTap = true;
this.nextStepCallback = waitForTapCallback;
this.overlay.down = function () {
if (TutorialSystem.isWaitingForTap) {
TutorialSystem.isWaitingForTap = false;
TutorialSystem.nextStepCallback();
}
};
}
},
clearMessage: function clearMessage() {
if (this.pulsingElement) {
tween.stop(this.pulsingElement);
if (this.pulsingElement.scale) {
tween.stop(this.pulsingElement.scale);
}
if (this.pulsingElement.originalScaleX) {
this.pulsingElement.scale.set(this.pulsingElement.originalScaleX, this.pulsingElement.originalScaleY);
}
if (this.pulsingElementOriginalParent) {
this.pulsingElementOriginalParent.addChild(this.pulsingElement);
this.pulsingElementOriginalParent = null;
}
this.pulsingElement = null;
}
if (this.overlay) {
if (this.overlay.parent) {
this.overlay.parent.removeChild(this.overlay);
}
this.overlay = null;
this.messageBox = null;
}
},
pulseAnimation: function pulseAnimation(element) {
var self = this;
var originalScaleX = element.originalScaleX || (element.scale ? element.scale.x : 1);
var originalScaleY = element.originalScaleY || (element.scale ? element.scale.y : 1);
element.originalScaleX = originalScaleX;
element.originalScaleY = originalScaleY;
tween(element.scale, {
x: originalScaleX * 1.1,
y: originalScaleY * 1.1
}, {
duration: 700,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (element.parent) {
tween(element.scale, {
x: originalScaleX,
y: originalScaleY
}, {
duration: 700,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (element.parent) {
self.pulseAnimation(element);
}
}
});
}
}
});
},
explainDrag: function explainDrag() {
var self = this;
var cardInHand = gameState.playerHand.find(function (c) {
return c !== null;
});
if (cardInHand) {
self.showMessage("You've been dealt 5 cards. Drag one to an empty slot on your board to play it.", cardInHand, self.advanceStep.bind(self));
}
},
waitForDrag: function waitForDrag() {
this.isWaitingForDrag = true;
},
explainDiscard: function explainDiscard() {
this.showMessage("You can also drag cards to the draw button to sell them for a refund.\n\nTap to continue.", discardAreaContainer, this.advanceStep.bind(this));
},
spawnFirstEnemy: function spawnFirstEnemy() {
this.showMessage("Excellent! A card on the board will automatically attack enemies. Let's see it in action!", null, function () {
TutorialSystem.clearMessage();
ChipSpawner.spawnChip(1, true);
TutorialSystem.advanceStep();
});
},
waitForKill: function waitForKill() {
this.isWaitingForChipKill = true;
},
explainMerge: function explainMerge() {
this.showMessage("Well done! You can level up cards by merging them. Drag a card onto another of the SAME SUIT or SAME VALUE to merge them.\nNote: When you merge, the new card will be a random value and suit, but its level will increase!", null, function () {
TutorialSystem.clearMessage();
var cardIndices = [];
for (var i = 0; i < gameState.playerHand.length; i++) {
if (gameState.playerHand[i]) {
cardIndices.push(i);
}
}
if (cardIndices.length >= 2) {
var index1 = cardIndices[0];
var index2 = cardIndices[1];
var oldCard1 = gameState.playerHand[index1];
var oldCard2 = gameState.playerHand[index2];
// Make them same suit to be mergeable
var newCardData1 = {
suit: 'spades',
value: 'A',
id: 'spades_A'
};
var newCardData2 = {
suit: 'spades',
value: 'K',
id: 'spades_K'
};
if (oldCard1 && oldCard1.parent) {
oldCard1.parent.removeChild(oldCard1);
}
if (oldCard2 && oldCard2.parent) {
oldCard2.parent.removeChild(oldCard2);
}
placeCardInHand(new Card(newCardData1), index1);
placeCardInHand(new Card(newCardData2), index2);
}
TutorialSystem.advanceStep();
});
},
waitForMerge: function waitForMerge() {
this.isWaitingForMerge = true;
},
explainJoker: function explainJoker() {
this.showMessage("Fantastic! Jokers are special. They can merge with ANY card to raise its level.", null, function () {
TutorialSystem.clearMessage();
// Place a Joker in hand
var jokerData = {
suit: 'joker',
value: 'black',
id: 'joker_black'
};
placeCardInHand(new Card(jokerData), 2); // Place it in the middle of the hand
TutorialSystem.advanceStep();
});
},
instructJokerMerge: function instructJokerMerge() {
var self = this;
var jokerInHand = gameState.playerHand[2];
if (jokerInHand) {
var message = "Now drag the Joker from your hand onto a card on your board to level it up!";
// We pulse the joker, as that's the one to drag.
this.showMessage(message, jokerInHand, function () {
self.clearMessage();
self.isWaitingForJokerMerge = true;
});
} else {
// Failsafe
this.advanceStep();
}
},
startWaves: function startWaves() {
this.showMessage("Perfect! Merging is key to victory. Now, a real wave is coming. The AI will also start playing.", null, function () {
TutorialSystem.clearMessage();
gameReady = true;
AISystem.thinkDelay = 240;
TutorialSystem.advanceStep();
});
},
explainPokerHands: function explainPokerHands() {
LK.setTimeout(function () {
TutorialSystem.showMessage("Placing cards in rows creates poker hands, like 'pairs' or 'straights', giving them a massive damage boost!", null, function () {
TutorialSystem.clearMessage();
TutorialSystem.advanceStep();
});
}, 8000);
},
guaranteedWin: function guaranteedWin() {},
guideToShop: function guideToShop() {
var items = this.startScreenItems;
var message = "You won! As a reward, you get a FREE mod. Go to the shop to claim it.";
this.setupPostGameGuide(message, items.shop.button, [items.battle.button, items.mods.button]);
var shopClickHandler = function shopClickHandler() {
items.shop.down();
TutorialSystem.clearMessage();
[items.battle.button, items.mods.button, items.shop.button].forEach(function (b) {
b.interactive = true;
b.alpha = 1;
});
TutorialSystem.advanceStep();
};
items.shop.button.down = shopClickHandler;
items.shop.icon.down = shopClickHandler;
},
guideToRandomMod: function guideToRandomMod() {
var items = this.startScreenItems.shopItems;
var message = "Tap the glowing item to get your free random mod!";
var originalPrice = storage.randomModPrice;
storage.randomModPrice = 0;
items.priceText.setText('$0');
this.showMessage(message, null);
if (this.overlay) {
// Hide the dark background and make overlay non-interactive to allow clicks on shop
this.overlay.getChildAt(0).visible = false;
this.overlay.interactive = false;
}
items.overlay.interactive = items.asset.interactive = true;
var originalBuy = items.buyButtonDown;
items.overlay.down = items.asset.down = function () {
items.overlay.down = items.asset.down = function () {}; // Prevent multi-click
originalBuy();
storage.randomModPrice = originalPrice;
LK.setTimeout(function () {
TutorialSystem.clearMessage();
TutorialSystem.advanceStep();
}, 3500);
};
},
guideToModsScreen: function guideToModsScreen() {
var items = this.startScreenItems;
items.shopContainer.visible = false;
var message = "Awesome! Now let's equip your new mod. Go to the mods screen.";
this.setupPostGameGuide(message, items.mods.button, [items.battle.button, items.shop.button]);
var modsClickHandler = function modsClickHandler() {
items.mods.down();
TutorialSystem.clearMessage();
[items.battle.button, items.mods.button, items.shop.button].forEach(function (b) {
b.interactive = true;
b.alpha = 1;
});
TutorialSystem.advanceStep();
};
items.mods.button.down = modsClickHandler;
items.mods.icon.down = modsClickHandler;
},
explainEquipMod: function explainEquipMod() {
// First, manually refresh the visual state of all mods in the grid
// because the grid was created before the player owned the new mod.
var modsContainer = this.startScreenItems.modsContainer;
// The gridContainer is the last major element added to the modsContainer.
var gridContainer = modsContainer.children[modsContainer.children.length - 1];
if (gridContainer && gridContainer.children.length > 0) {
var suitModAssets = ['burnHeartMod', 'chipsDiamondMod', 'spreadClubMod', 'freezeSpadeMod', 'unityHeartsMod', 'gamblerDiamondsMod', 'slowClubsMod', 'mineSpadesMod', 'investmentHeartsMod', 'boostDiamondsMod', 'ricochetClubsMod', 'deathSpadesMod'];
var childrenPerMod = 4; // Assumes cell, shadow, mod asset, name text
for (var modIndex = 0; modIndex < suitModAssets.length; modIndex++) {
var assetId = suitModAssets[modIndex];
var isOwned = ModSystem.isModOwned(assetId);
var cellIndex = modIndex * childrenPerMod;
var modAssetIndex = cellIndex + 2;
if (gridContainer.children[cellIndex] && gridContainer.children[modAssetIndex]) {
var cell = gridContainer.children[cellIndex];
var modAsset = gridContainer.children[modAssetIndex];
if (isOwned) {
modAsset.tint = 0xffffff;
modAsset.alpha = 1.0;
cell.tint = 0xffffff;
} else {
modAsset.tint = 0x555555;
modAsset.alpha = 0.6;
cell.tint = 0x888888;
}
}
}
}
var message = "This screen shows all mods. Tap your new mod, then tap 'Equip' to use it in battle!";
this.showMessage(message, null, function () {
TutorialSystem.clearMessage();
var originalEquip = ModSystem.equipMod;
ModSystem.equipMod = function (modAssetId) {
originalEquip.call(ModSystem, modAssetId);
TutorialSystem.advanceStep();
ModSystem.equipMod = originalEquip;
};
});
// Make the tutorial overlay transparent so the mods screen is fully visible.
if (this.overlay) {
this.overlay.getChildAt(0).visible = false;
}
},
completeTutorial: function completeTutorial() {
this.showMessage("Tutorial complete! You're ready for battle. Good luck!", null, function () {
TutorialSystem.clearMessage();
storage.tutorialCompleted = true;
TutorialSystem.isActive = false;
var items = TutorialSystem.startScreenItems;
[items.battle, items.mods, items.shop].forEach(function (item) {
if (item && item.button && item.icon) {
item.button.interactive = true;
item.button.alpha = 1.0;
item.icon.interactive = true;
item.icon.alpha = 1.0;
}
});
TutorialSystem.startScreenItems.modsContainer.visible = false;
TutorialSystem.startScreenItems.battle.down();
});
},
setupPostGameGuide: function setupPostGameGuide(message, elementToPulse, elementsToDisable) {
this.clearMessage();
this.showMessage(message, null, null, true); // isPostGame = true
var items = this.startScreenItems;
// Disable other buttons and their icons
elementsToDisable.forEach(function (el) {
el.interactive = false;
el.alpha = 0.5;
if (el === items.battle.button) {
items.battle.icon.interactive = false;
items.battle.icon.alpha = 0.5;
} else if (el === items.mods.button) {
items.mods.icon.interactive = false;
items.mods.icon.alpha = 0.5;
} else if (el === items.shop.button) {
items.shop.icon.interactive = false;
items.shop.icon.alpha = 0.5;
}
});
// Enable the target button and its icon
elementToPulse.interactive = true;
elementToPulse.alpha = 1.0;
var iconToPulse = null;
if (elementToPulse === items.shop.button) {
iconToPulse = items.shop.icon;
} else if (elementToPulse === items.mods.button) {
iconToPulse = items.mods.icon;
}
if (iconToPulse) {
iconToPulse.interactive = true;
iconToPulse.alpha = 1.0;
}
// Bring the container with all buttons on top of the overlay
if (this.overlay && elementToPulse.parent && elementToPulse.parent.parent) {
elementToPulse.parent.parent.addChild(elementToPulse.parent);
}
this.pulsingElement = elementToPulse;
this.pulseAnimation(this.pulsingElement);
}
};
function placeCardInHand(card, index) {
var handWidth = 5 * DEAL_SLOT_WIDTH + 4 * 30;
var handStartX = (SCREEN_WIDTH - handWidth) / 2;
var slotX = handStartX + index * DEAL_SLOT_WIDTH + index * 30 + DEAL_SLOT_WIDTH / 2;
var slotY = PLAYER_DEAL_AREA_Y + DEAL_SLOT_HEIGHT / 2;
card.activate(slotX, slotY, false, true);
uiLayer.addChild(card);
gameState.playerHand[index] = card;
}
function displayHandInfo() {
// Show current poker hand evaluations for debugging
for (var row = 0; row < PLAY_AREA_ROWS; row++) {
var handEval = evaluateRowHand(row, true);
console.log('Player Row ' + row + ':', handEval.type, 'Multiplier:', handEval.multiplier);
}
}
var background = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0
});
background.x = SCREEN_WIDTH / 2;
background.y = 50;
background.visible = false;
gameLayer.addChild(background);
var coopBackground = LK.getAsset('coopBackground', {
anchorX: 0.5,
anchorY: 0
});
coopBackground.x = SCREEN_WIDTH / 2;
coopBackground.y = 50;
coopBackground.visible = false;
gameLayer.addChild(coopBackground);
// Place two chipracks side by side in the center at the top of the screen
var chipRack1 = LK.getAsset('chipRack', {
anchorX: 0.5,
anchorY: 0
});
var rackWidth = chipRack1.width;
var rackHeight = chipRack1.height;
chipRack1.x = SCREEN_WIDTH / 2 - rackWidth / 2;
chipRack1.y = 60 - rackHeight * 0.75 + 30;
chipRack1.visible = false;
gameLayer.addChild(chipRack1);
gameElements.push(chipRack1);
var chipRack2 = LK.getAsset('chipRack', {
anchorX: 0.5,
anchorY: 0
});
chipRack2.x = SCREEN_WIDTH / 2 + rackWidth / 2;
chipRack2.y = 60 - rackHeight * 0.75 + 30;
chipRack2.visible = false;
gameLayer.addChild(chipRack2);
gameElements.push(chipRack2);
var border = LK.getAsset('border', {
anchorX: 0.5,
anchorY: 0.5
});
border.x = SCREEN_WIDTH / 2;
border.y = SCREEN_HEIGHT / 2;
border.visible = false;
gameLayer.addChild(border);
gameElements.push(border);
var bottomBar = LK.getAsset('bottomBar', {
anchorX: 0.5,
anchorY: 1
});
bottomBar.x = SCREEN_WIDTH / 2;
bottomBar.y = SCREEN_HEIGHT;
bottomBar.visible = false;
gameLayer.addChild(bottomBar);
gameElements.push(bottomBar);
// Show start screen instead of initializing game immediately
createStartScreen();
// Add active chips container below the bottom bar
gameLayer.addChildAt(activePlayerChipsContainer, gameLayer.getChildIndex(bottomBar));
gameLayer.addChildAt(activeAIChipsContainer, gameLayer.getChildIndex(bottomBar));
// Add chipStack asset above chips display
var chipStack = LK.getAsset('chipStack', {
anchorX: 0.5,
anchorY: 1
});
chipStack.x = playerChipsText.x + playerChipsText.width / 2;
chipStack.y = playerChipsText.y - 10;
chipStack.visible = false;
uiLayer.addChild(chipStack);
gameElements.push(chipStack);
;
;
A long rack of different colored poker chips seen from above. Anime style.. In-Game asset. 2d. High contrast. No shadows
A graphic for the center of a joker card.
a 2:3 format thin black border with nothing in the center. In-Game asset. 2d. High contrast. No shadows
A small white explosion particle.. In-Game asset. 2d. High contrast. No shadows
Make the blue a lighter blue.
Make this in a white instead of blue. Keep everything else the same.
A couple different sized stacks of these chips beside each other.
Just the spade from this picture with a blue snowflake in the middle of it.
Just the heart from this picture with a flame in the cent t of it.
Just the club from this picture with 1. **Fan/Spray Symbol** - Three or more lines radiating outward from a central point, yellow in color, in the center of the club.
Just the diamond from this picture with a dollar sign in the center
A white circle with a lightening gradient towards the edge.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A simple golden line break.. In-Game asset. 2d. High contrast. No shadows
A fanned card hand that shows a royal flush in spades. Anime style. In-Game asset. 2d. High contrast. No shadows
An SVG of the word 'Battle'. text in yellow with a black outline. In-Game asset. 2d. High contrast. No shadows
change the text to say "Mods"
The four card suits arranged in 2x2 grid layout, no lines. Anime style. In-Game asset. 2d. High contrast. No shadows
A single ice crystal. anime style. In-Game asset. 2d. High contrast. No shadows
Change the text to say ‘Refund’. Change the cards to a trash can.
A completely blank playing card with textured surface. Slightly used edges with a couple nicks out of it. Black background. In-Game asset. 2d. High contrast. No shadows
A 3:2 ratio rectangular green button that says “PvP” using this yellow font.
Change the text to say ‘Co-op’
Change the font to say ‘Victory!’
Change the text to say ‘Defeat!’
A 2:3 ratio rectangular picture that shows two card playing cats in a casino very close face to face with teeth bared and fists clenched as if they’re about to fight. Each cat has a different card suit pattern on the fur of their forehead. One is wearing a suit and the other is wearing tan leather jacket with a striped tank top underneath. Anime style.. In-Game asset. 2d. High contrast. No shadows
Show these same cats smiling and instead of clenched fists they’re grasping hands because they’re friends.
Incorporate these two cats heads into a game logo for a poker based tower defense that includes the name “Double Down Defense”. Put their heads offset on either side with eyes open and looking at the logo.
A small treasure chest with poker themed graphics on it. Anime style. In-Game asset. 2d. High contrast. No shadows
The hearts card suit symbol with two linked hearts in the center of it. Anime style.. In-Game asset. 2d. High contrast. No shadows
The diamond card suit with a coin in the center. The coin has a ‘2X’ in the center. Anime style.. In-Game asset. 2d. High contrast. No shadows
Just the club from this picture with a clock in the center.
Just the spade from this image with a land mine in the center of it.
Just the mine from this image.
Just the heart from this image with a piggy bank in the center.
Just the diamond from this picture with a sword with a small arrow pointing up in the center of the diamond.
Just the club from this picture with an icon in the center of it that represents a projectile bouncing at an angle off of a surface.
Just the spade with a skull in the center of it. Anime style.
This chest with the top open and nothing inside.
Change the text to say Shop
An old style cash register. The numeric read out says 7.77. Anime style.. In-Game asset. 2d. High contrast. No shadows
A giant question mark. Anime style.. In-Game asset. 2d. High contrast. No shadows
A shield with a spade and heart card suit coat of arms on it with a sword crossed downwards, behind it. icon. Anime style.. In-Game asset. 2d. High contrast. No shadows
Change the text to say ‘Draw’
The back of a playing card. Blue pattern. Anime style.. In-Game asset. 2d. High contrast. No shadows
The back of a playing card. Red pattern with a heart in the center. Anime style.. In-Game asset. 2d. High contrast. No shadows