/**** * Classes ****/ // Ensure ranged sound exists // Simple Button Class var Button = Container.expand(function (text, config) { var self = Container.call(this); // Methods must be defined before event handlers if called internally self.setText = function (newText) { self.label.setText(newText); }; self.setEnabled = function (enabled) { self.interactive = enabled; // Control interactability // LK engine doesn't directly use obj.interactive for event routing like standard PIXI, // but good practice. Use alpha for visual cue. self.alpha = enabled ? 1.0 : 0.5; }; // --- Initialize --- self.config = config || {}; var width = self.config.width || 300; var height = self.config.height || 100; var bgColor = self.config.bgColor || 0x555555; var textColor = self.config.textColor || '#FFFFFF'; var textSize = self.config.textSize || 40; var bg = self.attachAsset('buttonBg', { width: width, height: height, // color: bgColor, // Shape color is set at init time, cannot override here anchorX: 0.5, anchorY: 0.5 }); // Tint the background shape if a color was provided in config bg.tint = bgColor; self.label = new Text2(text, { size: textSize, fill: textColor }); self.label.anchor.set(0.5, 0.5); self.addChild(self.label); // Placeholder for the action, assign this after creating an instance self.onClick = null; // Must be assigned externally // LK engine automatically calls self.down if attached and interactive self.down = function (x, y, obj) { if (!self.interactive) { return; } // Respect the enabled state // Visual feedback for press self.scale.set(0.95); }; // LK engine automatically calls self.up if attached and interactive self.up = function (x, y, obj) { if (!self.interactive) { return; } // Respect the enabled state self.scale.set(1.0); // Restore scale // Check bounds roughly (optional, prevents firing if dragged off significantly) if (x >= -width / 2 && x <= width / 2 && y >= -height / 2 && y <= height / 2) { if (self.onClick) { self.onClick(); // Execute the assigned action } } }; // Initial state self.interactive = true; // Default to enabled return self; }); // Class for the Golden Piggy var GoldenPiggy = Container.expand(function () { var self = Container.call(this); // Constants var LIFETIME_TICKS = 5 * 60; // 5 seconds lifespan (at 60 FPS) // Properties self.ticksAlive = 0; self.isCollected = false; // --- Methods --- // Called when the piggy is tapped/clicked self.collect = function () { if (self.isCollected) { return; } // Prevent double collection self.isCollected = true; gold += 35; // Updated gold amount updateGoldDisplay(); // Optional: Play a sound effect try { // Reuse recruit sound or add a dedicated 'coin' sound if available LK.getSound('recruit').play(); } catch (e) { console.log("Sound error playing piggy collect sound: " + e); } // Remove the piggy visually (can be done in game.update) // self.visible = false; // Or self.destroy() handled by game.update console.log("Golden Piggy collected! +50 gold."); }; // LK engine calls this automatically if attached self.up = function (x, y, obj) { // Check bounds roughly to ensure the tap was on the piggy // Assuming piggy is roughly 100x100 as per asset if (x >= -self.graphics.width / 2 && x <= self.graphics.width / 2 && y >= -self.graphics.height / 2 && y <= self.graphics.height / 2) { self.collect(); } }; // Called by game.update self.update = function () { if (self.isCollected) { return; // Stop updating if collected } self.ticksAlive++; if (self.ticksAlive > LIFETIME_TICKS) { // Mark for removal if lifetime expires self.isCollected = true; // Use the same flag to trigger removal console.log("Golden Piggy disappeared."); } // Optional: Add subtle movement like bobbing up and down self.graphics.y = Math.sin(self.ticksAlive / 20) * 10; // Bobbing effect }; // --- Initialize --- self.graphics = self.attachAsset('Goldpiggy', { anchorX: 0.5, anchorY: 0.5 }); self.interactive = true; // Make it tappable return self; }); // var tween = LK.import('@upit/tween.v1'); // Not strictly needed for MVP // Base class for all units var Unit = Container.expand(function (config) { var self = Container.call(this); // Define methods first self.findTarget = function (enemyArray) { var closestTarget = null; var minDistance = Infinity; // Make sure enemyArray is defined and is an array if (!enemyArray || !Array.isArray(enemyArray)) { return null; } for (var i = 0; i < enemyArray.length; i++) { var enemy = enemyArray[i]; if (enemy.isDead) { continue; } // Using horizontal distance for simplicity var distance = Math.abs(self.x - enemy.x); if (distance < minDistance) { minDistance = distance; closestTarget = enemy; } } if (closestTarget && minDistance <= self.attackRange) { return closestTarget; // Return target if it's within attack range } else { return null; // No target in range } }; self.attack = function () { if (self.target && !self.target.isDead && self.attackCooldown <= 0) { try { // Defensive coding for sound playback LK.getSound(self.attackSoundId).play(); } catch (e) { console.log("Sound error playing " + self.attackSoundId + ": " + e); } self.target.takeDamage(self.attackPower); self.attackCooldown = self.attackCooldownTime; } }; self.move = function () { // Move towards the enemy side self.x += self.speed * (self.isEnemy ? -1 : 1); // Basic boundary check if (self.x < -100 || self.x > GAME_WIDTH + 100) { self.die(); // Remove if goes way off screen } }; self.takeDamage = function (damage) { if (self.isDead) { return; } // Already dead self.hp -= damage; // Optional: Add a visual effect like tinting red briefly // LK.effects.flashObject(self, 0xFF0000, 100); // Flashing the container might work if (self.hp <= 0) { // Award gold if an enemy dies from player attack if (self.isEnemy) { // Award gold based on unit type/strength gold += 5; // Base gold reward updateGoldDisplay(); // Update the display immediately } self.die(); } }; self.die = function () { if (!self.isDead) { try { // Defensive coding for sound playback LK.getSound('unitDeath').play(); } catch (e) { console.log("Sound error playing unitDeath: " + e); } self.isDead = true; // It will be removed from the game and array in game.update } }; // LK calls this automatically if the instance is attached to the stage/game self.update = function (enemyArray) { // This method requires the global `isBattling` to be accessible. if (self.isDead || !isBattling) { return; } // Don't update if dead or battle not active // Update cooldown if (self.attackCooldown > 0) { self.attackCooldown--; } // Find target self.target = self.findTarget(enemyArray); // Act based on target if (self.target) { self.isMoving = false; // Stop moving when target is in range self.attack(); } else { self.isMoving = true; // No target in range, resume moving } // Move if needed if (self.isMoving) { self.move(); } self.lastX = self.x; // Update last known state }; // --- Initialize --- self.isEnemy = config.isEnemy; self.hp = config.hp; self.maxHp = config.hp; // Store max HP self.attackPower = config.attackPower; self.attackRange = config.attackRange; self.attackCooldownTime = config.attackCooldownTime || 60; // Default 1 sec cooldown self.speed = config.speed; self.assetId = config.assetId; self.attackSoundId = config.attackSoundId || 'attackMelee'; self.unitType = config.unitType; // Store the unit type identifier // Attach graphics *after* properties are set self.graphics = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); // State variables self.target = null; self.attackCooldown = 0; // Start ready to attack self.isMoving = true; self.isDead = false; self.lastX = 0; // Initialize lastX, will be set correctly on spawn // Upgrade logic removed - replaced by global research system }); // WebGL Shader initialization var WebGLHelper = Container.expand(function () { var self = Container.call(this); // Method to initialize WebGL shaders self.initShaders = function (renderer) { if (!renderer || !renderer.gl) { return; } try { var createShader = function createShader(gl, source, type) { var shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('WebGL shader compilation failed: ' + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; }; var gl = renderer.gl; // Vertex shader var vertexShaderSource = "\n\t\t\t\tattribute vec2 aVertexPosition;\n\t\t\t\tattribute vec2 aTextureCoord;\n\t\t\t\tuniform mat3 projectionMatrix;\n\t\t\t\tvarying vec2 vTextureCoord;\n\t\t\t\tvoid main(void) {\n\t\t\t\t\tgl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);\n\t\t\t\t\tvTextureCoord = aTextureCoord;\n\t\t\t\t}"; // Fragment shader var fragmentShaderSource = "\n\t\t\t\tprecision mediump float;\n\t\t\t\tvarying vec2 vTextureCoord;\n\t\t\t\tuniform sampler2D uSampler;\n\t\t\t\tvoid main(void) {\n\t\t\t\t\tgl_FragColor = texture2D(uSampler, vTextureCoord);\n\t\t\t\t}"; // Create shader program var vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER); var fragmentShader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER); if (!vertexShader || !fragmentShader) { return; } // Create program var program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('WebGL program linking failed: ' + gl.getProgramInfoLog(program)); gl.deleteProgram(program); return; } return program; } catch (e) { console.error('WebGL shader initialization error:', e); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x101020 // Dark background for the war theme }); /**** * Game Code ****/ // Button background // Blue ellipse // Green box // Gray ellipse // White box // Define logical assets for units and UI. // LK Engine automatically creates assets based on usage. // Game constants and configuration // Added assets for new human units // Placeholder ID // Placeholder ID // Placeholder ID // Ensure melee sound exists var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var PLAYER_SPAWN_X = 150; var ENEMY_SPAWN_X = GAME_WIDTH - 150; var SPAWN_Y_START = GAME_HEIGHT * 0.3; // Start spawning units lower down var SPAWN_Y_END = GAME_HEIGHT * 0.8; // End spawning units lower down var SPAWN_Y_STEP = 100; // Vertical distance between spawned units // Research configuration var MILITIA_RESEARCH_COST = 50; var SLINGER_RESEARCH_COST = 75; var SHIELDED_RESEARCH_COST = 60; // Cost to research Shielded Skeleton var SWORDSMAN_RESEARCH_COST = 120; // Cost to research Swordsman var ARCHER_RESEARCH_COST = 150; // Cost to research Archer var HOPLITE_RESEARCH_COST = 180; // Cost to research Hoplite var militiaResearchComplete = false; // Tier 1 Melee (Rookie) var slingerResearchComplete = false; // Tier 1 Ranged (Trainee) var swordsmanResearchComplete = false; // Tier 2 Melee var archerResearchComplete = false; // Tier 2 Ranged var shieldedResearchComplete = false; // Tier 1 Defender var hopliteResearchComplete = false; // Tier 2 Defender // Unit stats lookup table var UNIT_STATS = { skeletonMilitia: { hp: 50, attackPower: 5, attackRange: 80, speed: 1.5, assetId: 'skeletonMilitia', cost: 10, // Cost to recruit base militia attackSoundId: 'attackMelee' }, // Note: skeletonRookie stats are defined below, its cost is for recruitment *after* research. skeletonSlinger: { hp: 30, attackPower: 7, attackRange: 400, speed: 1.2, assetId: 'skeletonSlinger', cost: 15, // Cost to recruit base slinger attackCooldownTime: 90, attackSoundId: 'attackRanged' }, // Note: skeletonTraineeArcher stats are defined below, its cost is for recruitment *after* research. humanFarmer: { hp: 40, attackPower: 4, attackRange: 70, speed: 1.3, assetId: 'humanFarmer', attackSoundId: 'attackMelee' }, humanSlinger: { hp: 25, attackPower: 6, attackRange: 380, speed: 1.1, assetId: 'humanSlinger', attackCooldownTime: 90, attackSoundId: 'attackRanged' }, // --- New Human Units --- humanGuard: { hp: 80, // Tougher than Farmer attackPower: 7, attackRange: 85, // Slightly longer reach than militia speed: 1.2, assetId: 'humanGuard', attackSoundId: 'attackMelee' }, humanMilitiaArcher: { hp: 35, // Similar to Slinger attackPower: 8, // Slightly more damage than Slinger attackRange: 420, // Slightly longer range speed: 1.3, assetId: 'humanMilitiaArcher', attackCooldownTime: 80, // Slightly faster fire rate attackSoundId: 'attackRanged' }, // --- New Elite Human Units --- humanEliteGuard: { hp: 120, // Much tougher than Guard attackPower: 12, // Significantly more damage than Guard attackRange: 90, // Slightly longer reach than Guard speed: 1.2, // Same speed as Guard assetId: 'humanEliteguard', attackSoundId: 'attackMelee' }, humanArcher: { hp: 45, // Tougher than Militia Archer attackPower: 11, // More damage than Militia Archer attackRange: 450, // Better range than Militia Archer speed: 1.3, // Same speed as Militia Archer assetId: 'HumanArcher', attackCooldownTime: 70, // Faster fire rate than Militia Archer attackSoundId: 'attackRanged' }, // --- New King's Guard (Melee Tier 3) --- "King's guard": { hp: 180, // Very tough attackPower: 15, // High damage attackRange: 100, // Good reach speed: 1.4, // Slightly faster than Elite Guard assetId: 'humanEliteguard', // Reusing asset, could be new if available attackSoundId: 'attackMelee' }, // --- New Crossbowman (Ranged Tier 3) --- "Crossbowman": { hp: 60, // Fragile attackPower: 18, // Very high damage attackRange: 500, // Long range speed: 1.1, // Slower than Archer assetId: 'skeletonCrossbowman', // Reusing asset, could be new if available attackCooldownTime: 60, // Fast attack attackSoundId: 'attackRanged' }, // --- New King's Shield (Defender Tier 3) --- "King's shield": { hp: 300, // Extremely tough attackPower: 8, // Moderate damage attackRange: 110, // Excellent reach speed: 1.3, // Average speed for defender assetId: 'humanGuardian', // Reusing asset, could be new if available attackSoundId: 'attackMelee' }, // --- New Bomber (Artillery Tier 1) --- "Bomber": { hp: 20, // Very fragile attackPower: 20, // High AoE damage (conceptually) attackRange: 700, // Very long range speed: 0.6, // Very slow assetId: 'skeletonBomber', // Reusing asset, could be new if available attackCooldownTime: 180, // Very slow attack rate attackSoundId: 'attackRanged' // Placeholder, ideally an explosion sound }, // --- New Cannoneer (Artillery Tier 2) --- "Cannoneer": { hp: 35, // Fragile attackPower: 30, // Very high AoE damage attackRange: 800, // Extremely long range speed: 0.5, // Even slower assetId: 'skeletonPumpkinCatapult', // Reusing asset, could be new if available attackCooldownTime: 240, // Very slow attack rate attackSoundId: 'attackRanged' // Placeholder }, // --- New Trebuchet (Artillery Tier 3) --- "Trebuchet": { hp: 50, // Still fragile attackPower: 50, // Massive AoE damage attackRange: 900, // Supreme range speed: 0.4, // Extremely slow assetId: 'skeletonDragon', // Reusing asset, could be new if available attackCooldownTime: 300, // Extremely slow attack rate attackSoundId: 'attackRanged' // Placeholder }, // -- Special Human Units -- humanLocalprotector: { hp: 150, // Very tough defender unit attackPower: 6, // Moderate damage attackRange: 90, // Melee range speed: 1.5, // Same speed as melee units assetId: 'humanLocalprotector', attackSoundId: 'attackMelee' }, humanGuardian: { hp: 220, // Extremely tough defender unit attackPower: 10, // Good damage for a defender attackRange: 100, // Slightly better melee range speed: 1.5, // Same speed as melee units assetId: 'humanGuardian', attackSoundId: 'attackMelee' }, // --- Researched Player Units --- skeletonRookie: { hp: 70, attackPower: 8, attackRange: 90, speed: 1.5, // Match militia speed to prevent getting faster when upgraded assetId: 'skeletonRookie', // Use the dedicated rookie asset cost: 20, attackSoundId: 'attackMelee' }, // Reusing militia asset for now skeletonTraineeArcher: { hp: 40, attackPower: 10, attackRange: 450, speed: 1.2, // Match slinger speed to prevent getting faster when upgraded assetId: 'skeletonTraineearcher', // Use the dedicated trainee archer asset cost: 25, attackCooldownTime: 70, attackSoundId: 'attackRanged' }, // --- Tier 2 Researched Player Units --- skeletonSwordsman: { hp: 100, // Stronger than Rookie attackPower: 12, // Higher damage attackRange: 100, // Slightly better reach speed: 1.5, // Match militia/rookie speed to prevent getting faster when upgraded assetId: 'skeletonSwordsman', cost: 35, // Cost after research attackSoundId: 'attackMelee' }, skeletonArcher: { hp: 55, // More durable than Trainee attackPower: 14, // Significantly more damage attackRange: 500, // Longer range speed: 1.2, // Match slinger/trainee speed to prevent getting faster when upgraded assetId: 'skeletonArcher', cost: 40, // Cost after research attackCooldownTime: 60, // Faster attack speed attackSoundId: 'attackRanged' }, // --- Tier 1 & 2 Defender Player Units --- skeletonShielded: { // Tier 1 Defender hp: 120, // High health attackPower: 4, // Low damage attackRange: 80, // Melee range speed: 1.5, // Match militia/rookie speed assetId: 'skeletonShielded', cost: 25, // Cost after research attackSoundId: 'attackMelee' }, skeletonHoplite: { // Tier 2 Defender hp: 180, // Very high health attackPower: 7, // Slightly better damage attackRange: 90, // Melee range speed: 1.5, // Match militia/rookie/swordsman speed assetId: 'skeletonHolpite', cost: 45, // Cost after research attackSoundId: 'attackMelee' }, // --- Tier 3 Researched Player Units --- skeletonEliteswordsman: { hp: 150, // Stronger than Swordsman attackPower: 18, // Significantly higher damage attackRange: 110, // Slightly better reach speed: 1.5, // Match previous melee speed assetId: 'skeletonEliteswordsman', cost: 60, // Cost after research attackSoundId: 'attackMelee' }, skeletonCrossbowman: { hp: 75, // More durable than Archer attackPower: 20, // High damage attackRange: 550, // Longest range speed: 1.2, // Match previous ranged speed assetId: 'skeletonCrossbowman', cost: 70, // Cost after research attackCooldownTime: 50, // Even faster attack speed attackSoundId: 'attackRanged' }, skeletonDarkKnight: { hp: 250, // Extremely high health attackPower: 12, // Better damage than Hoplite attackRange: 100, // Melee range speed: 1.5, // Match previous melee speed assetId: 'skeletonDarkKnight', cost: 80, // Cost after research attackSoundId: 'attackMelee' }, // --- Tier 1-3 Artillery Player Units --- skeletonBomber: { // Tier 1 Artillery hp: 20, // Very fragile attackPower: 15, // High damage attackRange: 600, // Long range speed: 0.8, // Very slow assetId: 'skeletonBomber', cost: 30, // Cost after research attackCooldownTime: 120, // Slow attack speed attackSoundId: 'attackRanged' // Placeholder, maybe a 'boom' sound later }, skeletonPumpkinCatapult: { // Tier 2 Artillery hp: 40, // Still fragile attackPower: 25, // Very high damage attackRange: 700, // Longer range speed: 0.7, // Even slower assetId: 'skeletonPumpkinCatapult', cost: 55, // Cost after research attackCooldownTime: 150, // Slower attack speed attackSoundId: 'attackRanged' // Placeholder }, skeletonDragon: { // Tier 3 Artillery hp: 80, // Moderately durable for artillery attackPower: 40, // Extremely high damage attackRange: 800, // Very long range speed: 1.0, // Slightly faster than others assetId: 'skeletonDragon', cost: 100, // Cost after research attackCooldownTime: 180, // Very slow attack speed attackSoundId: 'attackRanged' // Placeholder } }; // Research configuration var SHIELDED_RESEARCH_COST = 60; var HOPLITE_RESEARCH_COST = 180; // Research state var shieldedResearchComplete = false; // Tier 1 Defender var hopliteResearchComplete = false; // Tier 2 Defender // Tier 3 Research Costs var ELITESWORDSMAN_RESEARCH_COST = 250; var CROSSBOWMAN_RESEARCH_COST = 300; var DARKKNIGHT_RESEARCH_COST = 350; // Tier 3 Research State var eliteSwordsmanResearchComplete = false; var crossbowmanResearchComplete = false; var darkKnightResearchComplete = false; // Artillery Research Costs var BOMBER_RESEARCH_COST = 80; var CATAPULT_RESEARCH_COST = 200; var DRAGON_RESEARCH_COST = 400; // Artillery Research State var bomberResearchComplete = false; var catapultResearchComplete = false; var dragonResearchComplete = false; // Game state variables - Defined globally for access within functions and update loop var gold = 10; var playerUnits = []; var enemyUnits = []; var isBattling = false; var battleCounter = 0; // Tracks the number of battles won var currentEnemyConfig = {}; // Store the config for the current battle var goldenPiggyInstance = null; // Holds the active Golden Piggy instance // Initial player army composition (can be modified by recruitment) // Using an object for easier modification by type var playerArmyRoster = { skeletonMilitia: 2, skeletonSlinger: 1 }; // --- UI Elements --- // Gold Display var goldTxt = new Text2('Gold: 0', { size: 60, fill: 0xFFD700 }); goldTxt.anchor.set(1.0, 0); // Anchor top-right goldTxt.x = -50; // Offset from the right edge goldTxt.y = 20; // Offset from the top edge // Avoid top-left (reserved for LK menu), top-right is fine. LK.gui.topRight.addChild(goldTxt); // Village Counter Display var villageTxt = new Text2('Village: 1', { size: 60, fill: 0xFFD700 }); villageTxt.anchor.set(0.0, 0); // Anchor top-left villageTxt.x = 50; // Offset from the left edge villageTxt.y = 20; // Offset from the top edge LK.gui.topLeft.addChild(villageTxt); // Buttons Container (for organizing buttons at the bottom) var buttonContainer = new Container(); buttonContainer.x = GAME_WIDTH / 2; buttonContainer.y = GAME_HEIGHT - 200; // Position buttons lower game.addChild(buttonContainer); // Add to the main game scene // Start Battle Button var startButton = new Button('Start Battle', { width: 400, height: 120, textSize: 50, bgColor: 0x008000 }); // Greenish button startButton.y = -80; // Position above the recruitment buttons buttonContainer.addChild(startButton); // Recruitment Buttons (Example: Militia) var recruitMilitiaButton = new Button('Recruit Militia (10g)', { width: 400, height: 100, textSize: 40, bgColor: 0x404040 }); recruitMilitiaButton.x = -330; // Position further left for three buttons recruitMilitiaButton.y = 80; // Position below start button buttonContainer.addChild(recruitMilitiaButton); // Example: Slinger recruitment button var recruitSlingerButton = new Button('Recruit Slinger (15g)', { width: 400, height: 100, textSize: 40, bgColor: 0x404040 }); recruitSlingerButton.x = 330; // Position further right for three buttons recruitSlingerButton.y = 80; // Position below start button buttonContainer.addChild(recruitSlingerButton); // Defender recruitment button var recruitDefenderButton = new Button('Recruit Defender (25g)', { width: 400, height: 100, textSize: 40, bgColor: 0x404040 }); recruitDefenderButton.x = 0; // Center position recruitDefenderButton.y = 80; // Same row as other recruitment buttons buttonContainer.addChild(recruitDefenderButton); // Research Buttons var researchMilitiaButton = new Button('Research Rookie (50g)', { width: 400, height: 100, textSize: 40, bgColor: 0x004080 // Blueish button }); researchMilitiaButton.x = -220; // Position left, above recruitment researchMilitiaButton.y = -220; // Position above recruitment buttons buttonContainer.addChild(researchMilitiaButton); var researchSlingerButton = new Button('Research Trainee (75g)', { width: 400, height: 100, textSize: 40, bgColor: 0x004080 // Blueish button }); researchSlingerButton.x = 220; // Position right, above recruitment researchSlingerButton.y = -220; // Position above recruitment buttons buttonContainer.addChild(researchSlingerButton); // Tier 2 Research Buttons (Initially hidden) var researchSwordsmanButton = new Button('Research Swordsman (' + SWORDSMAN_RESEARCH_COST + 'g)', { width: 400, height: 100, textSize: 35, // Slightly smaller text if needed bgColor: 0x800080 // Purpleish button }); researchSwordsmanButton.x = -220; researchSwordsmanButton.y = -350; // Position above Tier 1 research researchSwordsmanButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchSwordsmanButton); var researchArcherButton = new Button('Research Archer (' + ARCHER_RESEARCH_COST + 'g)', { width: 400, height: 100, textSize: 35, // Slightly smaller text if needed bgColor: 0x800080 // Purpleish button }); researchArcherButton.x = 220; researchArcherButton.y = -350; // Position above Tier 1 research researchArcherButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchArcherButton); buttonContainer.addChild(researchArcherButton); // Defender Research Buttons var researchShieldedButton = new Button('Research Shielded (' + SHIELDED_RESEARCH_COST + 'g)', { width: 400, height: 100, textSize: 35, bgColor: 0x6A5ACD // SlateBlue button }); researchShieldedButton.x = 0; // Position center, above Tier 1 Melee/Ranged researchShieldedButton.y = -220; // Same level as Tier 1 Melee/Ranged research buttonContainer.addChild(researchShieldedButton); var researchHopliteButton = new Button('Research Hoplite (' + HOPLITE_RESEARCH_COST + 'g)', { width: 400, height: 100, textSize: 35, bgColor: 0x483D8B // DarkSlateBlue button }); researchHopliteButton.x = 0; // Position center, above Tier 2 Melee/Ranged researchHopliteButton.y = -350; // Same level as Tier 2 Melee/Ranged research researchHopliteButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchHopliteButton); // Adjust positions to accommodate the third column (Defender research) // Tier 1 Research researchMilitiaButton.x = -440; // Move left researchMilitiaButton.y = -220; researchSlingerButton.x = 440; // Move right researchSlingerButton.y = -220; researchShieldedButton.x = 0; // Center researchShieldedButton.y = -220; // Tier 2 Research researchSwordsmanButton.x = -440; // Move left researchSwordsmanButton.y = -350; researchArcherButton.x = 440; // Move right researchArcherButton.y = -350; researchHopliteButton.x = 0; // Center researchHopliteButton.y = -350; // Adjust Start Button position further up startButton.y = -480; // Move start button further up // Tier 3 Research Buttons (Initially hidden) var researchEliteSwordsmanButton = new Button('Research Elite (' + ELITESWORDSMAN_RESEARCH_COST + 'g)', { width: 400, height: 100, textSize: 35, bgColor: 0xFF4500 // Orangish-Red button }); researchEliteSwordsmanButton.x = -440; // Position left, above Tier 2 researchEliteSwordsmanButton.y = -480; // Position above Tier 2 research researchEliteSwordsmanButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchEliteSwordsmanButton); var researchCrossbowmanButton = new Button('Research Crossbow (' + CROSSBOWMAN_RESEARCH_COST + 'g)', { width: 400, height: 100, textSize: 35, bgColor: 0xFF4500 // Orangish-Red button }); researchCrossbowmanButton.x = 440; // Position right, above Tier 2 researchCrossbowmanButton.y = -480; // Position above Tier 2 research researchCrossbowmanButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchCrossbowmanButton); var researchDarkKnightButton = new Button('Research D.Knight (' + DARKKNIGHT_RESEARCH_COST + 'g)', { width: 400, height: 100, textSize: 35, bgColor: 0x8B0000 // DarkRed button }); researchDarkKnightButton.x = 0; // Position center, above Tier 2 researchDarkKnightButton.y = -480; // Position above Tier 2 research researchDarkKnightButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchDarkKnightButton); // Adjust Start Button position even further up to accommodate Tier 3 research startButton.y = -610; // Move start button further up // --- Artillery Research Buttons --- var researchBomberButton = new Button('Research Bomber (' + BOMBER_RESEARCH_COST + 'g)', { width: 380, // Slightly narrower for 4 columns height: 100, textSize: 35, bgColor: 0x8A2BE2 // BlueViolet }); researchBomberButton.x = 220; // Position between Defender and Ranged (Right side) researchBomberButton.y = -220; // Tier 1 row researchBomberButton.visible = true; // Initially visible buttonContainer.addChild(researchBomberButton); var researchCatapultButton = new Button('Research Catapult (' + CATAPULT_RESEARCH_COST + 'g)', { width: 380, height: 100, textSize: 35, bgColor: 0x9932CC // DarkOrchid }); researchCatapultButton.x = 220; // Position between Defender and Ranged (Right side) researchCatapultButton.y = -350; // Tier 2 row researchCatapultButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchCatapultButton); var researchDragonButton = new Button('Research Dragon (' + DRAGON_RESEARCH_COST + 'g)', { width: 380, height: 100, textSize: 35, bgColor: 0xBA55D3 // MediumOrchid }); researchDragonButton.x = 220; // Position between Defender and Ranged (Right side) researchDragonButton.y = -480; // Tier 3 row researchDragonButton.visible = false; // Hidden until prerequisite met buttonContainer.addChild(researchDragonButton); // --- Adjust existing button positions for 4 columns --- var buttonXOffsetLarge = 660; // For outer buttons (Melee, Ranged) var buttonXOffsetSmall = 220; // For inner buttons (Defender, Artillery) // Research Buttons researchMilitiaButton.x = -buttonXOffsetLarge; // Far Left researchMilitiaButton.width = 380; researchShieldedButton.x = -buttonXOffsetSmall; // Mid Left // Fix: Used correct variable name researchShieldedButton.width = 380; // Fix: Used correct variable name researchBomberButton.x = buttonXOffsetSmall; // Mid Right (new) researchBomberButton.width = 380; researchSlingerButton.x = buttonXOffsetLarge; // Far Right researchSlingerButton.width = 380; researchSwordsmanButton.x = -buttonXOffsetLarge; // Far Left researchSwordsmanButton.width = 380; researchHopliteButton.x = -buttonXOffsetSmall; // Mid Left researchHopliteButton.width = 380; researchCatapultButton.x = buttonXOffsetSmall; // Mid Right (new) researchCatapultButton.width = 380; researchArcherButton.x = buttonXOffsetLarge; // Far Right researchArcherButton.width = 380; researchEliteSwordsmanButton.x = -buttonXOffsetLarge; // Far Left researchEliteSwordsmanButton.width = 380; researchDarkKnightButton.x = -buttonXOffsetSmall; // Mid Left researchDarkKnightButton.width = 380; researchDragonButton.x = buttonXOffsetSmall; // Mid Right (new) researchDragonButton.width = 380; researchCrossbowmanButton.x = buttonXOffsetLarge; // Far Right researchCrossbowmanButton.width = 380; // Recruitment Buttons recruitMilitiaButton.x = -buttonXOffsetLarge; // Far Left recruitMilitiaButton.width = 380; recruitDefenderButton.x = -buttonXOffsetSmall; // Mid Left recruitDefenderButton.width = 380; // recruitArtilleryButton needs to be created first, then positioned recruitSlingerButton.x = buttonXOffsetLarge; // Far Right recruitSlingerButton.width = 380; // Adjust Start Button position further up startButton.y = -610; // Keep this adjustment // --- Add Artillery Recruitment Button --- var recruitArtilleryButton = new Button('Research Required', { // Initial text width: 380, // Match other buttons height: 100, textSize: 35, // Match research buttons bgColor: 0x404040 }); recruitArtilleryButton.x = buttonXOffsetSmall; // Position Mid Right recruitArtilleryButton.y = 80; // Same row as other recruitment buttons recruitArtilleryButton.setEnabled(false); // Initially disabled buttonContainer.addChild(recruitArtilleryButton); // --- Game Logic Functions --- function updateGoldDisplay() { goldTxt.setText('Gold: ' + gold); var canAffordMilitiaResearch = gold >= MILITIA_RESEARCH_COST; var canAffordSlingerResearch = gold >= SLINGER_RESEARCH_COST; var canAffordShieldedResearch = gold >= SHIELDED_RESEARCH_COST; // New Defender cost check var canAffordSwordsmanResearch = gold >= SWORDSMAN_RESEARCH_COST; var canAffordArcherResearch = gold >= ARCHER_RESEARCH_COST; var canAffordHopliteResearch = gold >= HOPLITE_RESEARCH_COST; var canAffordEliteSwordsmanResearch = gold >= ELITESWORDSMAN_RESEARCH_COST; var canAffordCrossbowmanResearch = gold >= CROSSBOWMAN_RESEARCH_COST; var canAffordDarkKnightResearch = gold >= DARKKNIGHT_RESEARCH_COST; // Artillery research cost checks var canAffordBomberResearch = gold >= BOMBER_RESEARCH_COST; var canAffordCatapultResearch = gold >= CATAPULT_RESEARCH_COST; var canAffordDragonResearch = gold >= DRAGON_RESEARCH_COST; // --- Update Research Buttons --- // Tier 1: Militia -> Rookie if (militiaResearchComplete) { researchMilitiaButton.visible = false; // Hide Tier 1 when complete researchMilitiaButton.setEnabled(false); } else { researchMilitiaButton.visible = true; researchMilitiaButton.setEnabled(canAffordMilitiaResearch && !isBattling); } // Tier 1: Slinger -> Trainee if (slingerResearchComplete) { researchSlingerButton.visible = false; // Hide Tier 1 when complete researchSlingerButton.setEnabled(false); } else { researchSlingerButton.visible = true; researchSlingerButton.setEnabled(canAffordSlingerResearch && !isBattling); } // Tier 1: Defender -> Shielded Skeleton if (shieldedResearchComplete) { researchShieldedButton.visible = false; researchShieldedButton.setEnabled(false); } else { researchShieldedButton.visible = true; researchShieldedButton.setEnabled(canAffordShieldedResearch && !isBattling); } // Tier 2: Rookie -> Swordsman if (swordsmanResearchComplete) { researchSwordsmanButton.visible = false; // Hide Tier 2 when complete researchSwordsmanButton.setEnabled(false); } else { // Show Tier 2 only if Tier 1 is complete researchSwordsmanButton.visible = militiaResearchComplete; researchSwordsmanButton.setEnabled(militiaResearchComplete && canAffordSwordsmanResearch && !isBattling); } // Tier 2: Trainee -> Archer if (archerResearchComplete) { researchArcherButton.visible = false; // Hide Tier 2 when complete researchArcherButton.setEnabled(false); } else { // Show Tier 2 only if Tier 1 is complete researchArcherButton.visible = slingerResearchComplete; researchArcherButton.setEnabled(slingerResearchComplete && canAffordArcherResearch && !isBattling); } // Tier 2: Shielded -> Hoplite if (hopliteResearchComplete) { researchHopliteButton.visible = false; researchHopliteButton.setEnabled(false); } else { // Show Tier 2 only if Tier 1 is complete researchHopliteButton.visible = shieldedResearchComplete; researchHopliteButton.setEnabled(shieldedResearchComplete && canAffordHopliteResearch && !isBattling); } // Tier 3: Swordsman -> Elite Swordsman if (eliteSwordsmanResearchComplete) { researchEliteSwordsmanButton.visible = false; researchEliteSwordsmanButton.setEnabled(false); } else { // Show Tier 3 only if Tier 2 is complete researchEliteSwordsmanButton.visible = swordsmanResearchComplete; researchEliteSwordsmanButton.setEnabled(swordsmanResearchComplete && canAffordEliteSwordsmanResearch && !isBattling); } // Tier 3: Archer -> Crossbowman if (crossbowmanResearchComplete) { researchCrossbowmanButton.visible = false; researchCrossbowmanButton.setEnabled(false); } else { // Show Tier 3 only if Tier 2 is complete researchCrossbowmanButton.visible = archerResearchComplete; researchCrossbowmanButton.setEnabled(archerResearchComplete && canAffordCrossbowmanResearch && !isBattling); } // Tier 3: Hoplite -> Dark Knight if (darkKnightResearchComplete) { researchDarkKnightButton.visible = false; researchDarkKnightButton.setEnabled(false); } else { // Show Tier 3 only if Tier 2 is complete researchDarkKnightButton.visible = hopliteResearchComplete; researchDarkKnightButton.setEnabled(hopliteResearchComplete && canAffordDarkKnightResearch && !isBattling); } // Tier 1: Artillery -> Bomber if (bomberResearchComplete) { researchBomberButton.visible = false; researchBomberButton.setEnabled(false); } else { researchBomberButton.visible = true; researchBomberButton.setEnabled(canAffordBomberResearch && !isBattling); } // Tier 2: Bomber -> Catapult if (catapultResearchComplete) { researchCatapultButton.visible = false; researchCatapultButton.setEnabled(false); } else { // Show Tier 2 only if Tier 1 is complete researchCatapultButton.visible = bomberResearchComplete; researchCatapultButton.setEnabled(bomberResearchComplete && canAffordCatapultResearch && !isBattling); } // Tier 3: Catapult -> Dragon if (dragonResearchComplete) { researchDragonButton.visible = false; researchDragonButton.setEnabled(false); } else { // Show Tier 3 only if Tier 2 is complete researchDragonButton.visible = catapultResearchComplete; researchDragonButton.setEnabled(catapultResearchComplete && canAffordDragonResearch && !isBattling); } // --- Update Recruitment Buttons --- // Determine current highest researched MELEE unit var currentMilitiaType = 'skeletonMilitia'; var militiaButtonText = 'Recruit Militia'; if (eliteSwordsmanResearchComplete) { currentMilitiaType = 'skeletonEliteswordsman'; militiaButtonText = 'Recruit Elite'; } else if (swordsmanResearchComplete) { currentMilitiaType = 'skeletonSwordsman'; militiaButtonText = 'Recruit Swordsman'; } else if (militiaResearchComplete) { currentMilitiaType = 'skeletonRookie'; militiaButtonText = 'Recruit Rookie'; } var currentMilitiaStats = UNIT_STATS[currentMilitiaType]; recruitMilitiaButton.setText(militiaButtonText + ' (' + currentMilitiaStats.cost + 'g)'); recruitMilitiaButton.setEnabled(gold >= currentMilitiaStats.cost && !isBattling); // Determine current highest researched RANGED unit var currentSlingerType = 'skeletonSlinger'; var slingerButtonText = 'Recruit Slinger'; if (crossbowmanResearchComplete) { currentSlingerType = 'skeletonCrossbowman'; slingerButtonText = 'Recruit Crossbow'; } else if (archerResearchComplete) { currentSlingerType = 'skeletonArcher'; slingerButtonText = 'Recruit Archer'; } else if (slingerResearchComplete) { currentSlingerType = 'skeletonTraineeArcher'; slingerButtonText = 'Recruit Trainee'; } var currentSlingerStats = UNIT_STATS[currentSlingerType]; recruitSlingerButton.setText(slingerButtonText + ' (' + currentSlingerStats.cost + 'g)'); recruitSlingerButton.setEnabled(gold >= currentSlingerStats.cost && !isBattling); // Determine current highest researched DEFENDER unit var currentDefenderType = null; var defenderButtonText = 'Research Required'; if (darkKnightResearchComplete) { currentDefenderType = 'skeletonDarkKnight'; defenderButtonText = 'Recruit D.Knight'; } else if (hopliteResearchComplete) { currentDefenderType = 'skeletonHoplite'; defenderButtonText = 'Recruit Hoplite'; } else if (shieldedResearchComplete) { currentDefenderType = 'skeletonShielded'; defenderButtonText = 'Recruit Shielded'; } // Update defender button state based on research status if (currentDefenderType) { var currentDefenderStats = UNIT_STATS[currentDefenderType]; recruitDefenderButton.setText(defenderButtonText + ' (' + currentDefenderStats.cost + 'g)'); recruitDefenderButton.setEnabled(gold >= currentDefenderStats.cost && !isBattling); } else { // No defender research completed yet recruitDefenderButton.setText('Research Required'); recruitDefenderButton.setEnabled(false); } // Determine current highest researched ARTILLERY unit var currentArtilleryType = null; var artilleryButtonText = 'Research Required'; if (dragonResearchComplete) { currentArtilleryType = 'skeletonDragon'; artilleryButtonText = 'Recruit Dragon'; } else if (catapultResearchComplete) { currentArtilleryType = 'skeletonPumpkinCatapult'; artilleryButtonText = 'Recruit Catapult'; } else if (bomberResearchComplete) { currentArtilleryType = 'skeletonBomber'; artilleryButtonText = 'Recruit Bomber'; } // Update artillery button state based on research status if (currentArtilleryType) { var currentArtilleryStats = UNIT_STATS[currentArtilleryType]; recruitArtilleryButton.setText(artilleryButtonText + ' (' + currentArtilleryStats.cost + 'g)'); recruitArtilleryButton.setEnabled(gold >= currentArtilleryStats.cost && !isBattling); } else { // No artillery research completed yet recruitArtilleryButton.setText('Research Required'); recruitArtilleryButton.setEnabled(false); // Ensure it's disabled } } function recruitUnit(unitType) { if (isBattling) { return; } // Cannot recruit during battle var stats = UNIT_STATS[unitType]; if (!stats) { console.error("Attempted to recruit unknown unit type:", unitType); return; } if (gold >= stats.cost) { gold -= stats.cost; try { // Defensive coding for sound playback LK.getSound('recruit').play(); } catch (e) { console.log("Sound error playing recruit: " + e); } // Safely increment the count in the roster playerArmyRoster[unitType] = (playerArmyRoster[unitType] || 0) + 1; console.log("Recruited " + unitType + ". Roster:", playerArmyRoster); updateGoldDisplay(); // Update UI after recruitment } else { console.log("Not enough gold to recruit " + unitType); // Optional: Show feedback to the player (e.g., flashing the gold red) LK.effects.flashObject(goldTxt, 0xFF0000, 300); } } function researchUnit(researchType) { if (isBattling) { return; } // Cannot research during battle var cost = 0; var cost = 0; var researchName = ''; var canResearch = false; var setResearchFlag = null; // Function to set the correct flag if (researchType === 'militia' && !militiaResearchComplete) { cost = MILITIA_RESEARCH_COST; researchName = 'Rookie'; canResearch = true; // No prerequisite setResearchFlag = function setResearchFlag() { militiaResearchComplete = true; }; } else if (researchType === 'slinger' && !slingerResearchComplete) { cost = SLINGER_RESEARCH_COST; researchName = 'Trainee Archer'; canResearch = true; // No prerequisite setResearchFlag = function setResearchFlag() { slingerResearchComplete = true; }; } else if (researchType === 'swordsman' && !swordsmanResearchComplete) { cost = SWORDSMAN_RESEARCH_COST; researchName = 'Swordsman'; canResearch = militiaResearchComplete; // Prerequisite: Rookie researched setResearchFlag = function setResearchFlag() { swordsmanResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Rookie first."); } } else if (researchType === 'archer' && !archerResearchComplete) { cost = ARCHER_RESEARCH_COST; researchName = 'Archer'; canResearch = slingerResearchComplete; // Prerequisite: Trainee researched setResearchFlag = function setResearchFlag() { archerResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Trainee Archer first."); } } else if (researchType === 'shielded' && !shieldedResearchComplete) { // New Defender Tier 1 cost = SHIELDED_RESEARCH_COST; researchName = 'Shielded Skeleton'; canResearch = true; // No prerequisite setResearchFlag = function setResearchFlag() { shieldedResearchComplete = true; }; } else if (researchType === 'hoplite' && !hopliteResearchComplete) { // New Defender Tier 2 cost = HOPLITE_RESEARCH_COST; researchName = 'Skeleton Hoplite'; canResearch = shieldedResearchComplete; // Prerequisite: Shielded researched setResearchFlag = function setResearchFlag() { hopliteResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Shielded Skeleton first."); } } else if (researchType === 'eliteswordsman' && !eliteSwordsmanResearchComplete) { // Tier 3 Melee cost = ELITESWORDSMAN_RESEARCH_COST; researchName = 'Elite Swordsman'; canResearch = swordsmanResearchComplete; // Prerequisite: Swordsman researched setResearchFlag = function setResearchFlag() { eliteSwordsmanResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Swordsman first."); } } else if (researchType === 'crossbowman' && !crossbowmanResearchComplete) { // Tier 3 Ranged cost = CROSSBOWMAN_RESEARCH_COST; researchName = 'Crossbowman'; canResearch = archerResearchComplete; // Prerequisite: Archer researched setResearchFlag = function setResearchFlag() { crossbowmanResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Archer first."); } } else if (researchType === 'darkknight' && !darkKnightResearchComplete) { // Tier 3 Defender cost = DARKKNIGHT_RESEARCH_COST; researchName = 'Dark Knight'; canResearch = hopliteResearchComplete; // Prerequisite: Hoplite researched setResearchFlag = function setResearchFlag() { darkKnightResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Hoplite first."); } } else if (researchType === 'bomber' && !bomberResearchComplete) { // Artillery Tier 1 cost = BOMBER_RESEARCH_COST; researchName = 'Skeleton Bomber'; canResearch = true; // No prerequisite setResearchFlag = function setResearchFlag() { bomberResearchComplete = true; }; } else if (researchType === 'catapult' && !catapultResearchComplete) { // Artillery Tier 2 cost = CATAPULT_RESEARCH_COST; researchName = 'Pumpkin Catapult'; canResearch = bomberResearchComplete; // Prerequisite: Bomber setResearchFlag = function setResearchFlag() { catapultResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Bomber first."); } } else if (researchType === 'dragon' && !dragonResearchComplete) { // Artillery Tier 3 cost = DRAGON_RESEARCH_COST; researchName = 'Skeleton Dragon'; canResearch = catapultResearchComplete; // Prerequisite: Catapult setResearchFlag = function setResearchFlag() { dragonResearchComplete = true; }; if (!canResearch) { console.log("Prerequisite not met: Research Catapult first."); } } else { console.log("Research already complete, prerequisite not met, or invalid type:", researchType); return; // Invalid type, already researched, or prereq not met } if (canResearch && gold >= cost) { gold -= cost; setResearchFlag(); // Set the specific research flag to true try { // Reuse upgrade sound, or create a specific 'research' sound LK.getSound('upgradeUnit').play(); // Assuming 'upgradeUnit' sound is suitable } catch (e) { console.log("Sound error playing research sound: " + e); } console.log("Research complete for:", researchName); updateGoldDisplay(); // Update UI immediately } else { console.log("Not enough gold to research " + researchName); LK.effects.flashObject(goldTxt, 0xFF0000, 300); } } function clearBattlefield() { // Remove all existing units from the game and arrays for (var i = playerUnits.length - 1; i >= 0; i--) { if (playerUnits[i] && playerUnits[i].destroy) { // Check if destroy exists playerUnits[i].destroy(); } } playerUnits = []; // Reset array for (var i = enemyUnits.length - 1; i >= 0; i--) { if (enemyUnits[i] && enemyUnits[i].destroy) { // Check if destroy exists enemyUnits[i].destroy(); } } enemyUnits = []; // Reset array // Remove the golden piggy if it exists if (goldenPiggyInstance !== null) { goldenPiggyInstance.destroy(); goldenPiggyInstance = null; } } function spawnUnit(unitType, isEnemy) { var stats = UNIT_STATS[unitType]; if (!stats) { console.error("Cannot spawn unknown unit type:", unitType); return null; } // Prepare config for the Unit class constructor var config = { isEnemy: isEnemy, hp: stats.hp, attackPower: stats.attackPower, attackRange: stats.attackRange, speed: stats.speed, assetId: stats.assetId, attackCooldownTime: stats.attackCooldownTime, attackSoundId: stats.attackSoundId, unitType: unitType // Pass the unit type }; // Create new instance of Unit var unit = new Unit(config); // Determine spawn side and array var armyArray = isEnemy ? enemyUnits : playerUnits; var spawnX = isEnemy ? ENEMY_SPAWN_X : PLAYER_SPAWN_X; // Calculate staggered Y position var currentY = SPAWN_Y_START + armyArray.length * SPAWN_Y_STEP; // Wrap Y position if it goes beyond the designated spawn area end if (currentY > SPAWN_Y_END) { var yRange = SPAWN_Y_END - SPAWN_Y_START; if (yRange <= 0) { yRange = SPAWN_Y_STEP; } // Avoid division by zero or negative range currentY = SPAWN_Y_START + armyArray.length * SPAWN_Y_STEP % yRange; } // Set initial position and state unit.x = spawnX; unit.y = currentY; unit.lastX = unit.x; // Initialize last state correctly // Add the unit to the game display and the logical array game.addChild(unit); armyArray.push(unit); return unit; // Return the created unit if needed elsewhere } function startBattle() { if (isBattling) { return; } // Prevent starting multiple battles console.log("Starting battle..."); isBattling = true; clearBattlefield(); // Remove any units from a previous battle // Define enemy forces for this battle (can be made dynamic later) if (Object.keys(currentEnemyConfig).length === 0) { // Only initialize on first battle currentEnemyConfig = { humanFarmer: 3, humanSlinger: 1 }; } // Otherwise use the already incremented config from previous win // Spawn Player Units based on the current roster console.log("Spawning player units:", playerArmyRoster); for (var unitType in playerArmyRoster) { if (playerArmyRoster.hasOwnProperty(unitType)) { // Good practice for iterating objects var count = playerArmyRoster[unitType]; for (var i = 0; i < count; i++) { spawnUnit(unitType, false); // Spawn player unit (not enemy) } } } // Spawn Enemy Units based on the current configuration console.log("Spawning enemy units:", currentEnemyConfig); for (var unitType in currentEnemyConfig) { if (currentEnemyConfig.hasOwnProperty(unitType)) { var count = currentEnemyConfig[unitType]; for (var i = 0; i < count; i++) { spawnUnit(unitType, true); // Spawn enemy unit } } } // Hide UI elements not relevant during battle buttonContainer.visible = false; // Ensure units arrays are populated before the first update runs console.log("Battle started. Player units:", playerUnits.length, "Enemy units:", enemyUnits.length); } function endBattle(playerWon) { // Check if the battle hasn't already ended to prevent multiple triggers if (!isBattling) { return; } console.log("Battle ended. Player won:", playerWon); isBattling = false; // Set state before doing anything else // Explicitly remove piggy when battle ends, BEFORE showing UI again. // This ensures it's properly cleaned up and allows a new one to spawn in subsequent battles. if (goldenPiggyInstance !== null) { goldenPiggyInstance.destroy(); goldenPiggyInstance = null; } // Show UI elements again buttonContainer.visible = true; if (playerWon) { var goldEarned = 50; // Example gold reward gold += goldEarned; try { // Defensive coding for sound playback LK.getSound('battleWon').play(); } catch (e) { console.log("Sound error playing battleWon: " + e); } console.log("Earned " + goldEarned + " gold. Total gold:", gold); // Update display immediately after awarding gold updateGoldDisplay(); battleCounter++; // Increment the battle counter on win villageTxt.setText('Village: ' + (battleCounter + 1)); // Update the village counter display console.log("Battle won! Proceeding to village: " + (battleCounter + 1)); // Humans gain more units when player wins battles for (var unitType in currentEnemyConfig) { if (currentEnemyConfig.hasOwnProperty(unitType)) { // Increase each existing unit type by 1-2 for the next battle currentEnemyConfig[unitType] += Math.floor(Math.random() * 2) + 1; } } // Introduce new units starting from the 5th battle (after winning the 4th) if (battleCounter >= 4) { // 4 wins means the next battle is the 5th // Add Guard if not present, or increase count if (!currentEnemyConfig.hasOwnProperty('humanGuard')) { currentEnemyConfig['humanGuard'] = 1; // Start with 1 } else { currentEnemyConfig['humanGuard'] += 1; // Add one more each subsequent win } // Add Militia Archer if not present, or increase count if (!currentEnemyConfig.hasOwnProperty('humanMilitiaArcher')) { currentEnemyConfig['humanMilitiaArcher'] = 1; // Start with 1 } else { currentEnemyConfig['humanMilitiaArcher'] += 1; // Add one more each subsequent win } console.log("Stronger human reinforcements (Guards, Militia Archers) have arrived!"); } // Introduce Local Protector starting from the 6th battle (after winning the 5th) if (battleCounter >= 5) { // 5 wins means the next battle is the 6th if (!currentEnemyConfig.hasOwnProperty('humanLocalprotector')) { currentEnemyConfig['humanLocalprotector'] = 1; // Start with 1 } else { currentEnemyConfig['humanLocalprotector'] += 1; // Add one more each win } console.log("Local Protector reinforcements have arrived!"); } // Introduce elite units starting from the 8th battle (after winning the 7th) if (battleCounter >= 7) { // 7 wins means the next battle is the 8th // Add Elite Guard if not present, or increase count if (!currentEnemyConfig.hasOwnProperty('humanEliteGuard')) { currentEnemyConfig['humanEliteGuard'] = 1; // Start with 1 } else { currentEnemyConfig['humanEliteGuard'] += 1; // Add one more each subsequent win } // Add Archer if not present, or increase count if (!currentEnemyConfig.hasOwnProperty('humanArcher')) { currentEnemyConfig['humanArcher'] = 1; // Start with 1 } else { currentEnemyConfig['humanArcher'] += 1; // Add one more each subsequent win } console.log("Elite human reinforcements (Elite Guards, Archers) have arrived!"); } // Introduce Guardian starting from the 9th battle (after winning the 8th) if (battleCounter >= 8) { // 8 wins means the next battle is the 9th if (!currentEnemyConfig.hasOwnProperty('humanGuardian')) { currentEnemyConfig['humanGuardian'] = 1; // Start with 1 } else { currentEnemyConfig['humanGuardian'] += 1; // Add one more each win } console.log("Guardian reinforcements have arrived!"); } // Introduce Bomber starting from the 13th battle (after winning the 12th) if (battleCounter >= 12) { if (!currentEnemyConfig.hasOwnProperty('Bomber')) { currentEnemyConfig['Bomber'] = 1; } else { currentEnemyConfig['Bomber'] += 1; } console.log("Bomber reinforcements have arrived!"); } // Introduce King's Guard starting from the 15th battle (after winning the 14th) if (battleCounter >= 14) { if (!currentEnemyConfig.hasOwnProperty("King's guard")) { currentEnemyConfig["King's guard"] = 1; } else { currentEnemyConfig["King's guard"] += 1; } console.log("King's Guard reinforcements have arrived!"); } // Introduce Crossbowman starting from the 16th battle (after winning the 15th) if (battleCounter >= 15) { if (!currentEnemyConfig.hasOwnProperty("Crossbowman")) { currentEnemyConfig["Crossbowman"] = 1; } else { currentEnemyConfig["Crossbowman"] += 1; } console.log("Crossbowman reinforcements have arrived!"); } // Introduce Cannoneer starting from the 20th battle (after winning the 19th) if (battleCounter >= 19) { if (!currentEnemyConfig.hasOwnProperty('Cannoneer')) { currentEnemyConfig['Cannoneer'] = 1; } else { currentEnemyConfig['Cannoneer'] += 1; } console.log("Cannoneer reinforcements have arrived!"); } // Introduce King's Shield starting from the 23rd battle (after winning the 22nd) if (battleCounter >= 22) { if (!currentEnemyConfig.hasOwnProperty("King's shield")) { currentEnemyConfig["King's shield"] = 1; } else { currentEnemyConfig["King's shield"] += 1; } console.log("King's Shield reinforcements have arrived!"); } // Introduce Trebuchet starting from the 26th battle (after winning the 25th) if (battleCounter >= 25) { if (!currentEnemyConfig.hasOwnProperty('Trebuchet')) { currentEnemyConfig['Trebuchet'] = 1; } else { currentEnemyConfig['Trebuchet'] += 1; } console.log("Trebuchet reinforcements have arrived!"); } console.log("Enemy forces have grown stronger for the next battle:", currentEnemyConfig); // Game continues, player can choose to recruit or battle again } else { console.log("Player lost the battle."); // Game over is handled by LK engine which resets the game state LK.showGameOver(); // Note: Gold and roster will reset unless storage plugin is used. } // Update button states based on new gold amount AFTER battle outcome is decided // This is especially important if the player lost, as LK.showGameOver might reset things // before this line runs if not careful. updateGoldDisplay() called inside win condition is safer. // If player loses, the game resets anyway, so updating buttons might not be needed here. if (playerWon) { updateGoldDisplay(); // Ensure buttons reflect new gold total after winning } } // --- Initialize Game State --- // Assign onClick handlers to buttons *after* they are created startButton.onClick = startBattle; // Assign the function reference recruitMilitiaButton.onClick = function () { var unitToRecruit = 'skeletonMilitia'; // Default if (eliteSwordsmanResearchComplete) { unitToRecruit = 'skeletonEliteswordsman'; } else if (swordsmanResearchComplete) { unitToRecruit = 'skeletonSwordsman'; } else if (militiaResearchComplete) { unitToRecruit = 'skeletonRookie'; } recruitUnit(unitToRecruit); }; recruitSlingerButton.onClick = function () { var unitToRecruit = 'skeletonSlinger'; // Default if (crossbowmanResearchComplete) { unitToRecruit = 'skeletonCrossbowman'; } else if (archerResearchComplete) { unitToRecruit = 'skeletonArcher'; } else if (slingerResearchComplete) { unitToRecruit = 'skeletonTraineeArcher'; } recruitUnit(unitToRecruit); }; recruitDefenderButton.onClick = function () { // Default to no unit (research required) var unitToRecruit = null; // Check research status to determine which defender unit to recruit if (darkKnightResearchComplete) { unitToRecruit = 'skeletonDarkKnight'; } else if (hopliteResearchComplete) { unitToRecruit = 'skeletonHoplite'; } else if (shieldedResearchComplete) { unitToRecruit = 'skeletonShielded'; } // Only attempt to recruit if a valid unit type is available if (unitToRecruit) { recruitUnit(unitToRecruit); } }; // Tier 1 Research Button Handlers researchMilitiaButton.onClick = function () { researchUnit('militia'); }; researchSlingerButton.onClick = function () { researchUnit('slinger'); }; // Tier 2 Research Button Handlers researchSwordsmanButton.onClick = function () { researchUnit('swordsman'); }; researchArcherButton.onClick = function () { researchUnit('archer'); }; // Defender Research Button Handlers researchShieldedButton.onClick = function () { researchUnit('shielded'); }; researchHopliteButton.onClick = function () { researchUnit('hoplite'); }; // Tier 3 Research Button Handlers researchEliteSwordsmanButton.onClick = function () { researchUnit('eliteswordsman'); }; researchCrossbowmanButton.onClick = function () { researchUnit('crossbowman'); }; researchDarkKnightButton.onClick = function () { researchUnit('darkknight'); }; // Artillery Research Button Handlers researchBomberButton.onClick = function () { researchUnit('bomber'); }; researchCatapultButton.onClick = function () { researchUnit('catapult'); }; researchDragonButton.onClick = function () { researchUnit('dragon'); }; // Artillery Recruitment Button Handler recruitArtilleryButton.onClick = function () { var unitToRecruit = null; if (dragonResearchComplete) { unitToRecruit = 'skeletonDragon'; } else if (catapultResearchComplete) { unitToRecruit = 'skeletonPumpkinCatapult'; } else if (bomberResearchComplete) { unitToRecruit = 'skeletonBomber'; } // Only attempt to recruit if a valid unit type is available if (unitToRecruit) { recruitUnit(unitToRecruit); } }; updateGoldDisplay(); // Set initial gold text and button states based on starting gold (10) and research state // --- Game Update Loop --- // This function is called by the LK engine automatically 60 times per second. game.update = function () { if (!isBattling) { // Game is idle, waiting for player action (recruit/start battle) // No unit updates needed. return; } // --- Battle Logic --- // Update Player Units (Iterate backwards for safe removal) for (var i = playerUnits.length - 1; i >= 0; i--) { var pUnit = playerUnits[i]; // Ensure unit exists and has methods before calling them if (!pUnit) { continue; } if (pUnit.isDead) { if (pUnit.destroy) { pUnit.destroy(); } // Remove graphics from game playerUnits.splice(i, 1); // Remove from logical array continue; // Move to the next unit } // Pass the array of enemies for target finding if (pUnit.update) { pUnit.update(enemyUnits); } } // Update Enemy Units (Iterate backwards for safe removal) for (var i = enemyUnits.length - 1; i >= 0; i--) { var eUnit = enemyUnits[i]; if (!eUnit) { continue; } if (eUnit.isDead) { if (eUnit.destroy) { eUnit.destroy(); } enemyUnits.splice(i, 1); continue; } // Pass the array of player units for target finding if (eUnit.update) { eUnit.update(playerUnits); } } // --- Golden Piggy Logic --- // Chance to spawn a piggy if one isn't already active if (isBattling && goldenPiggyInstance === null) { // Low chance per tick, e.g., 1 in 1000 ticks on average var PIGGY_SPAWN_CHANCE = 1 / (60 * 10); // Roughly once every 10 seconds average if (Math.random() < PIGGY_SPAWN_CHANCE) { console.log("Spawning Golden Piggy!"); goldenPiggyInstance = new GoldenPiggy(); // Spawn position (e.g., random y in the middle vertical third, center horizontally) var spawnY = GAME_HEIGHT * 0.33 + Math.random() * (GAME_HEIGHT * 0.33); goldenPiggyInstance.x = GAME_WIDTH / 2; goldenPiggyInstance.y = spawnY; game.addChild(goldenPiggyInstance); // Add to display } } // Update active piggy and remove if collected/timed out if (goldenPiggyInstance !== null) { if (goldenPiggyInstance.isCollected) { goldenPiggyInstance.destroy(); goldenPiggyInstance = null; } else { goldenPiggyInstance.update(); // Call piggy's own update logic } } // Check Win/Loss Conditions *after* all units have been updated for the tick // Only check if the battle is currently marked as active if (isBattling) { if (enemyUnits.length === 0 && playerUnits.length > 0) { // Player wins: All enemies are defeated, player still has units. endBattle(true); } else if (playerUnits.length === 0) { // Player loses: All player units are defeated. (Enemy count doesn't matter here) // We might get here even if enemies also died in the same tick. endBattle(false); } } }; // --- Input Handling --- // Button clicks are managed by the Button class instances via their `up` method. // No global game-level mouse/touch handlers needed for this MVP's interaction model.
/****
* Classes
****/
// Ensure ranged sound exists
// Simple Button Class
var Button = Container.expand(function (text, config) {
var self = Container.call(this);
// Methods must be defined before event handlers if called internally
self.setText = function (newText) {
self.label.setText(newText);
};
self.setEnabled = function (enabled) {
self.interactive = enabled; // Control interactability
// LK engine doesn't directly use obj.interactive for event routing like standard PIXI,
// but good practice. Use alpha for visual cue.
self.alpha = enabled ? 1.0 : 0.5;
};
// --- Initialize ---
self.config = config || {};
var width = self.config.width || 300;
var height = self.config.height || 100;
var bgColor = self.config.bgColor || 0x555555;
var textColor = self.config.textColor || '#FFFFFF';
var textSize = self.config.textSize || 40;
var bg = self.attachAsset('buttonBg', {
width: width,
height: height,
// color: bgColor, // Shape color is set at init time, cannot override here
anchorX: 0.5,
anchorY: 0.5
});
// Tint the background shape if a color was provided in config
bg.tint = bgColor;
self.label = new Text2(text, {
size: textSize,
fill: textColor
});
self.label.anchor.set(0.5, 0.5);
self.addChild(self.label);
// Placeholder for the action, assign this after creating an instance
self.onClick = null; // Must be assigned externally
// LK engine automatically calls self.down if attached and interactive
self.down = function (x, y, obj) {
if (!self.interactive) {
return;
} // Respect the enabled state
// Visual feedback for press
self.scale.set(0.95);
};
// LK engine automatically calls self.up if attached and interactive
self.up = function (x, y, obj) {
if (!self.interactive) {
return;
} // Respect the enabled state
self.scale.set(1.0); // Restore scale
// Check bounds roughly (optional, prevents firing if dragged off significantly)
if (x >= -width / 2 && x <= width / 2 && y >= -height / 2 && y <= height / 2) {
if (self.onClick) {
self.onClick(); // Execute the assigned action
}
}
};
// Initial state
self.interactive = true; // Default to enabled
return self;
});
// Class for the Golden Piggy
var GoldenPiggy = Container.expand(function () {
var self = Container.call(this);
// Constants
var LIFETIME_TICKS = 5 * 60; // 5 seconds lifespan (at 60 FPS)
// Properties
self.ticksAlive = 0;
self.isCollected = false;
// --- Methods ---
// Called when the piggy is tapped/clicked
self.collect = function () {
if (self.isCollected) {
return;
} // Prevent double collection
self.isCollected = true;
gold += 35; // Updated gold amount
updateGoldDisplay();
// Optional: Play a sound effect
try {
// Reuse recruit sound or add a dedicated 'coin' sound if available
LK.getSound('recruit').play();
} catch (e) {
console.log("Sound error playing piggy collect sound: " + e);
}
// Remove the piggy visually (can be done in game.update)
// self.visible = false; // Or self.destroy() handled by game.update
console.log("Golden Piggy collected! +50 gold.");
};
// LK engine calls this automatically if attached
self.up = function (x, y, obj) {
// Check bounds roughly to ensure the tap was on the piggy
// Assuming piggy is roughly 100x100 as per asset
if (x >= -self.graphics.width / 2 && x <= self.graphics.width / 2 && y >= -self.graphics.height / 2 && y <= self.graphics.height / 2) {
self.collect();
}
};
// Called by game.update
self.update = function () {
if (self.isCollected) {
return; // Stop updating if collected
}
self.ticksAlive++;
if (self.ticksAlive > LIFETIME_TICKS) {
// Mark for removal if lifetime expires
self.isCollected = true; // Use the same flag to trigger removal
console.log("Golden Piggy disappeared.");
}
// Optional: Add subtle movement like bobbing up and down
self.graphics.y = Math.sin(self.ticksAlive / 20) * 10; // Bobbing effect
};
// --- Initialize ---
self.graphics = self.attachAsset('Goldpiggy', {
anchorX: 0.5,
anchorY: 0.5
});
self.interactive = true; // Make it tappable
return self;
});
// var tween = LK.import('@upit/tween.v1'); // Not strictly needed for MVP
// Base class for all units
var Unit = Container.expand(function (config) {
var self = Container.call(this);
// Define methods first
self.findTarget = function (enemyArray) {
var closestTarget = null;
var minDistance = Infinity;
// Make sure enemyArray is defined and is an array
if (!enemyArray || !Array.isArray(enemyArray)) {
return null;
}
for (var i = 0; i < enemyArray.length; i++) {
var enemy = enemyArray[i];
if (enemy.isDead) {
continue;
}
// Using horizontal distance for simplicity
var distance = Math.abs(self.x - enemy.x);
if (distance < minDistance) {
minDistance = distance;
closestTarget = enemy;
}
}
if (closestTarget && minDistance <= self.attackRange) {
return closestTarget; // Return target if it's within attack range
} else {
return null; // No target in range
}
};
self.attack = function () {
if (self.target && !self.target.isDead && self.attackCooldown <= 0) {
try {
// Defensive coding for sound playback
LK.getSound(self.attackSoundId).play();
} catch (e) {
console.log("Sound error playing " + self.attackSoundId + ": " + e);
}
self.target.takeDamage(self.attackPower);
self.attackCooldown = self.attackCooldownTime;
}
};
self.move = function () {
// Move towards the enemy side
self.x += self.speed * (self.isEnemy ? -1 : 1);
// Basic boundary check
if (self.x < -100 || self.x > GAME_WIDTH + 100) {
self.die(); // Remove if goes way off screen
}
};
self.takeDamage = function (damage) {
if (self.isDead) {
return;
} // Already dead
self.hp -= damage;
// Optional: Add a visual effect like tinting red briefly
// LK.effects.flashObject(self, 0xFF0000, 100); // Flashing the container might work
if (self.hp <= 0) {
// Award gold if an enemy dies from player attack
if (self.isEnemy) {
// Award gold based on unit type/strength
gold += 5; // Base gold reward
updateGoldDisplay(); // Update the display immediately
}
self.die();
}
};
self.die = function () {
if (!self.isDead) {
try {
// Defensive coding for sound playback
LK.getSound('unitDeath').play();
} catch (e) {
console.log("Sound error playing unitDeath: " + e);
}
self.isDead = true;
// It will be removed from the game and array in game.update
}
};
// LK calls this automatically if the instance is attached to the stage/game
self.update = function (enemyArray) {
// This method requires the global `isBattling` to be accessible.
if (self.isDead || !isBattling) {
return;
} // Don't update if dead or battle not active
// Update cooldown
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
// Find target
self.target = self.findTarget(enemyArray);
// Act based on target
if (self.target) {
self.isMoving = false; // Stop moving when target is in range
self.attack();
} else {
self.isMoving = true; // No target in range, resume moving
}
// Move if needed
if (self.isMoving) {
self.move();
}
self.lastX = self.x; // Update last known state
};
// --- Initialize ---
self.isEnemy = config.isEnemy;
self.hp = config.hp;
self.maxHp = config.hp; // Store max HP
self.attackPower = config.attackPower;
self.attackRange = config.attackRange;
self.attackCooldownTime = config.attackCooldownTime || 60; // Default 1 sec cooldown
self.speed = config.speed;
self.assetId = config.assetId;
self.attackSoundId = config.attackSoundId || 'attackMelee';
self.unitType = config.unitType; // Store the unit type identifier
// Attach graphics *after* properties are set
self.graphics = self.attachAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// State variables
self.target = null;
self.attackCooldown = 0; // Start ready to attack
self.isMoving = true;
self.isDead = false;
self.lastX = 0; // Initialize lastX, will be set correctly on spawn
// Upgrade logic removed - replaced by global research system
});
// WebGL Shader initialization
var WebGLHelper = Container.expand(function () {
var self = Container.call(this);
// Method to initialize WebGL shaders
self.initShaders = function (renderer) {
if (!renderer || !renderer.gl) {
return;
}
try {
var createShader = function createShader(gl, source, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('WebGL shader compilation failed: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
};
var gl = renderer.gl;
// Vertex shader
var vertexShaderSource = "\n\t\t\t\tattribute vec2 aVertexPosition;\n\t\t\t\tattribute vec2 aTextureCoord;\n\t\t\t\tuniform mat3 projectionMatrix;\n\t\t\t\tvarying vec2 vTextureCoord;\n\t\t\t\tvoid main(void) {\n\t\t\t\t\tgl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);\n\t\t\t\t\tvTextureCoord = aTextureCoord;\n\t\t\t\t}";
// Fragment shader
var fragmentShaderSource = "\n\t\t\t\tprecision mediump float;\n\t\t\t\tvarying vec2 vTextureCoord;\n\t\t\t\tuniform sampler2D uSampler;\n\t\t\t\tvoid main(void) {\n\t\t\t\t\tgl_FragColor = texture2D(uSampler, vTextureCoord);\n\t\t\t\t}";
// Create shader program
var vertexShader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
var fragmentShader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
if (!vertexShader || !fragmentShader) {
return;
}
// Create program
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('WebGL program linking failed: ' + gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return;
}
return program;
} catch (e) {
console.error('WebGL shader initialization error:', e);
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x101020 // Dark background for the war theme
});
/****
* Game Code
****/
// Button background
// Blue ellipse
// Green box
// Gray ellipse
// White box
// Define logical assets for units and UI.
// LK Engine automatically creates assets based on usage.
// Game constants and configuration
// Added assets for new human units
// Placeholder ID
// Placeholder ID
// Placeholder ID
// Ensure melee sound exists
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var PLAYER_SPAWN_X = 150;
var ENEMY_SPAWN_X = GAME_WIDTH - 150;
var SPAWN_Y_START = GAME_HEIGHT * 0.3; // Start spawning units lower down
var SPAWN_Y_END = GAME_HEIGHT * 0.8; // End spawning units lower down
var SPAWN_Y_STEP = 100; // Vertical distance between spawned units
// Research configuration
var MILITIA_RESEARCH_COST = 50;
var SLINGER_RESEARCH_COST = 75;
var SHIELDED_RESEARCH_COST = 60; // Cost to research Shielded Skeleton
var SWORDSMAN_RESEARCH_COST = 120; // Cost to research Swordsman
var ARCHER_RESEARCH_COST = 150; // Cost to research Archer
var HOPLITE_RESEARCH_COST = 180; // Cost to research Hoplite
var militiaResearchComplete = false; // Tier 1 Melee (Rookie)
var slingerResearchComplete = false; // Tier 1 Ranged (Trainee)
var swordsmanResearchComplete = false; // Tier 2 Melee
var archerResearchComplete = false; // Tier 2 Ranged
var shieldedResearchComplete = false; // Tier 1 Defender
var hopliteResearchComplete = false; // Tier 2 Defender
// Unit stats lookup table
var UNIT_STATS = {
skeletonMilitia: {
hp: 50,
attackPower: 5,
attackRange: 80,
speed: 1.5,
assetId: 'skeletonMilitia',
cost: 10,
// Cost to recruit base militia
attackSoundId: 'attackMelee'
},
// Note: skeletonRookie stats are defined below, its cost is for recruitment *after* research.
skeletonSlinger: {
hp: 30,
attackPower: 7,
attackRange: 400,
speed: 1.2,
assetId: 'skeletonSlinger',
cost: 15,
// Cost to recruit base slinger
attackCooldownTime: 90,
attackSoundId: 'attackRanged'
},
// Note: skeletonTraineeArcher stats are defined below, its cost is for recruitment *after* research.
humanFarmer: {
hp: 40,
attackPower: 4,
attackRange: 70,
speed: 1.3,
assetId: 'humanFarmer',
attackSoundId: 'attackMelee'
},
humanSlinger: {
hp: 25,
attackPower: 6,
attackRange: 380,
speed: 1.1,
assetId: 'humanSlinger',
attackCooldownTime: 90,
attackSoundId: 'attackRanged'
},
// --- New Human Units ---
humanGuard: {
hp: 80,
// Tougher than Farmer
attackPower: 7,
attackRange: 85,
// Slightly longer reach than militia
speed: 1.2,
assetId: 'humanGuard',
attackSoundId: 'attackMelee'
},
humanMilitiaArcher: {
hp: 35,
// Similar to Slinger
attackPower: 8,
// Slightly more damage than Slinger
attackRange: 420,
// Slightly longer range
speed: 1.3,
assetId: 'humanMilitiaArcher',
attackCooldownTime: 80,
// Slightly faster fire rate
attackSoundId: 'attackRanged'
},
// --- New Elite Human Units ---
humanEliteGuard: {
hp: 120,
// Much tougher than Guard
attackPower: 12,
// Significantly more damage than Guard
attackRange: 90,
// Slightly longer reach than Guard
speed: 1.2,
// Same speed as Guard
assetId: 'humanEliteguard',
attackSoundId: 'attackMelee'
},
humanArcher: {
hp: 45,
// Tougher than Militia Archer
attackPower: 11,
// More damage than Militia Archer
attackRange: 450,
// Better range than Militia Archer
speed: 1.3,
// Same speed as Militia Archer
assetId: 'HumanArcher',
attackCooldownTime: 70,
// Faster fire rate than Militia Archer
attackSoundId: 'attackRanged'
},
// --- New King's Guard (Melee Tier 3) ---
"King's guard": {
hp: 180,
// Very tough
attackPower: 15,
// High damage
attackRange: 100,
// Good reach
speed: 1.4,
// Slightly faster than Elite Guard
assetId: 'humanEliteguard',
// Reusing asset, could be new if available
attackSoundId: 'attackMelee'
},
// --- New Crossbowman (Ranged Tier 3) ---
"Crossbowman": {
hp: 60,
// Fragile
attackPower: 18,
// Very high damage
attackRange: 500,
// Long range
speed: 1.1,
// Slower than Archer
assetId: 'skeletonCrossbowman',
// Reusing asset, could be new if available
attackCooldownTime: 60,
// Fast attack
attackSoundId: 'attackRanged'
},
// --- New King's Shield (Defender Tier 3) ---
"King's shield": {
hp: 300,
// Extremely tough
attackPower: 8,
// Moderate damage
attackRange: 110,
// Excellent reach
speed: 1.3,
// Average speed for defender
assetId: 'humanGuardian',
// Reusing asset, could be new if available
attackSoundId: 'attackMelee'
},
// --- New Bomber (Artillery Tier 1) ---
"Bomber": {
hp: 20,
// Very fragile
attackPower: 20,
// High AoE damage (conceptually)
attackRange: 700,
// Very long range
speed: 0.6,
// Very slow
assetId: 'skeletonBomber',
// Reusing asset, could be new if available
attackCooldownTime: 180,
// Very slow attack rate
attackSoundId: 'attackRanged' // Placeholder, ideally an explosion sound
},
// --- New Cannoneer (Artillery Tier 2) ---
"Cannoneer": {
hp: 35,
// Fragile
attackPower: 30,
// Very high AoE damage
attackRange: 800,
// Extremely long range
speed: 0.5,
// Even slower
assetId: 'skeletonPumpkinCatapult',
// Reusing asset, could be new if available
attackCooldownTime: 240,
// Very slow attack rate
attackSoundId: 'attackRanged' // Placeholder
},
// --- New Trebuchet (Artillery Tier 3) ---
"Trebuchet": {
hp: 50,
// Still fragile
attackPower: 50,
// Massive AoE damage
attackRange: 900,
// Supreme range
speed: 0.4,
// Extremely slow
assetId: 'skeletonDragon',
// Reusing asset, could be new if available
attackCooldownTime: 300,
// Extremely slow attack rate
attackSoundId: 'attackRanged' // Placeholder
},
// -- Special Human Units --
humanLocalprotector: {
hp: 150,
// Very tough defender unit
attackPower: 6,
// Moderate damage
attackRange: 90,
// Melee range
speed: 1.5,
// Same speed as melee units
assetId: 'humanLocalprotector',
attackSoundId: 'attackMelee'
},
humanGuardian: {
hp: 220,
// Extremely tough defender unit
attackPower: 10,
// Good damage for a defender
attackRange: 100,
// Slightly better melee range
speed: 1.5,
// Same speed as melee units
assetId: 'humanGuardian',
attackSoundId: 'attackMelee'
},
// --- Researched Player Units ---
skeletonRookie: {
hp: 70,
attackPower: 8,
attackRange: 90,
speed: 1.5,
// Match militia speed to prevent getting faster when upgraded
assetId: 'skeletonRookie',
// Use the dedicated rookie asset
cost: 20,
attackSoundId: 'attackMelee'
},
// Reusing militia asset for now
skeletonTraineeArcher: {
hp: 40,
attackPower: 10,
attackRange: 450,
speed: 1.2,
// Match slinger speed to prevent getting faster when upgraded
assetId: 'skeletonTraineearcher',
// Use the dedicated trainee archer asset
cost: 25,
attackCooldownTime: 70,
attackSoundId: 'attackRanged'
},
// --- Tier 2 Researched Player Units ---
skeletonSwordsman: {
hp: 100,
// Stronger than Rookie
attackPower: 12,
// Higher damage
attackRange: 100,
// Slightly better reach
speed: 1.5,
// Match militia/rookie speed to prevent getting faster when upgraded
assetId: 'skeletonSwordsman',
cost: 35,
// Cost after research
attackSoundId: 'attackMelee'
},
skeletonArcher: {
hp: 55,
// More durable than Trainee
attackPower: 14,
// Significantly more damage
attackRange: 500,
// Longer range
speed: 1.2,
// Match slinger/trainee speed to prevent getting faster when upgraded
assetId: 'skeletonArcher',
cost: 40,
// Cost after research
attackCooldownTime: 60,
// Faster attack speed
attackSoundId: 'attackRanged'
},
// --- Tier 1 & 2 Defender Player Units ---
skeletonShielded: {
// Tier 1 Defender
hp: 120,
// High health
attackPower: 4,
// Low damage
attackRange: 80,
// Melee range
speed: 1.5,
// Match militia/rookie speed
assetId: 'skeletonShielded',
cost: 25,
// Cost after research
attackSoundId: 'attackMelee'
},
skeletonHoplite: {
// Tier 2 Defender
hp: 180,
// Very high health
attackPower: 7,
// Slightly better damage
attackRange: 90,
// Melee range
speed: 1.5,
// Match militia/rookie/swordsman speed
assetId: 'skeletonHolpite',
cost: 45,
// Cost after research
attackSoundId: 'attackMelee'
},
// --- Tier 3 Researched Player Units ---
skeletonEliteswordsman: {
hp: 150,
// Stronger than Swordsman
attackPower: 18,
// Significantly higher damage
attackRange: 110,
// Slightly better reach
speed: 1.5,
// Match previous melee speed
assetId: 'skeletonEliteswordsman',
cost: 60,
// Cost after research
attackSoundId: 'attackMelee'
},
skeletonCrossbowman: {
hp: 75,
// More durable than Archer
attackPower: 20,
// High damage
attackRange: 550,
// Longest range
speed: 1.2,
// Match previous ranged speed
assetId: 'skeletonCrossbowman',
cost: 70,
// Cost after research
attackCooldownTime: 50,
// Even faster attack speed
attackSoundId: 'attackRanged'
},
skeletonDarkKnight: {
hp: 250,
// Extremely high health
attackPower: 12,
// Better damage than Hoplite
attackRange: 100,
// Melee range
speed: 1.5,
// Match previous melee speed
assetId: 'skeletonDarkKnight',
cost: 80,
// Cost after research
attackSoundId: 'attackMelee'
},
// --- Tier 1-3 Artillery Player Units ---
skeletonBomber: {
// Tier 1 Artillery
hp: 20,
// Very fragile
attackPower: 15,
// High damage
attackRange: 600,
// Long range
speed: 0.8,
// Very slow
assetId: 'skeletonBomber',
cost: 30,
// Cost after research
attackCooldownTime: 120,
// Slow attack speed
attackSoundId: 'attackRanged' // Placeholder, maybe a 'boom' sound later
},
skeletonPumpkinCatapult: {
// Tier 2 Artillery
hp: 40,
// Still fragile
attackPower: 25,
// Very high damage
attackRange: 700,
// Longer range
speed: 0.7,
// Even slower
assetId: 'skeletonPumpkinCatapult',
cost: 55,
// Cost after research
attackCooldownTime: 150,
// Slower attack speed
attackSoundId: 'attackRanged' // Placeholder
},
skeletonDragon: {
// Tier 3 Artillery
hp: 80,
// Moderately durable for artillery
attackPower: 40,
// Extremely high damage
attackRange: 800,
// Very long range
speed: 1.0,
// Slightly faster than others
assetId: 'skeletonDragon',
cost: 100,
// Cost after research
attackCooldownTime: 180,
// Very slow attack speed
attackSoundId: 'attackRanged' // Placeholder
}
};
// Research configuration
var SHIELDED_RESEARCH_COST = 60;
var HOPLITE_RESEARCH_COST = 180;
// Research state
var shieldedResearchComplete = false; // Tier 1 Defender
var hopliteResearchComplete = false; // Tier 2 Defender
// Tier 3 Research Costs
var ELITESWORDSMAN_RESEARCH_COST = 250;
var CROSSBOWMAN_RESEARCH_COST = 300;
var DARKKNIGHT_RESEARCH_COST = 350;
// Tier 3 Research State
var eliteSwordsmanResearchComplete = false;
var crossbowmanResearchComplete = false;
var darkKnightResearchComplete = false;
// Artillery Research Costs
var BOMBER_RESEARCH_COST = 80;
var CATAPULT_RESEARCH_COST = 200;
var DRAGON_RESEARCH_COST = 400;
// Artillery Research State
var bomberResearchComplete = false;
var catapultResearchComplete = false;
var dragonResearchComplete = false;
// Game state variables - Defined globally for access within functions and update loop
var gold = 10;
var playerUnits = [];
var enemyUnits = [];
var isBattling = false;
var battleCounter = 0; // Tracks the number of battles won
var currentEnemyConfig = {}; // Store the config for the current battle
var goldenPiggyInstance = null; // Holds the active Golden Piggy instance
// Initial player army composition (can be modified by recruitment)
// Using an object for easier modification by type
var playerArmyRoster = {
skeletonMilitia: 2,
skeletonSlinger: 1
};
// --- UI Elements ---
// Gold Display
var goldTxt = new Text2('Gold: 0', {
size: 60,
fill: 0xFFD700
});
goldTxt.anchor.set(1.0, 0); // Anchor top-right
goldTxt.x = -50; // Offset from the right edge
goldTxt.y = 20; // Offset from the top edge
// Avoid top-left (reserved for LK menu), top-right is fine.
LK.gui.topRight.addChild(goldTxt);
// Village Counter Display
var villageTxt = new Text2('Village: 1', {
size: 60,
fill: 0xFFD700
});
villageTxt.anchor.set(0.0, 0); // Anchor top-left
villageTxt.x = 50; // Offset from the left edge
villageTxt.y = 20; // Offset from the top edge
LK.gui.topLeft.addChild(villageTxt);
// Buttons Container (for organizing buttons at the bottom)
var buttonContainer = new Container();
buttonContainer.x = GAME_WIDTH / 2;
buttonContainer.y = GAME_HEIGHT - 200; // Position buttons lower
game.addChild(buttonContainer); // Add to the main game scene
// Start Battle Button
var startButton = new Button('Start Battle', {
width: 400,
height: 120,
textSize: 50,
bgColor: 0x008000
}); // Greenish button
startButton.y = -80; // Position above the recruitment buttons
buttonContainer.addChild(startButton);
// Recruitment Buttons (Example: Militia)
var recruitMilitiaButton = new Button('Recruit Militia (10g)', {
width: 400,
height: 100,
textSize: 40,
bgColor: 0x404040
});
recruitMilitiaButton.x = -330; // Position further left for three buttons
recruitMilitiaButton.y = 80; // Position below start button
buttonContainer.addChild(recruitMilitiaButton);
// Example: Slinger recruitment button
var recruitSlingerButton = new Button('Recruit Slinger (15g)', {
width: 400,
height: 100,
textSize: 40,
bgColor: 0x404040
});
recruitSlingerButton.x = 330; // Position further right for three buttons
recruitSlingerButton.y = 80; // Position below start button
buttonContainer.addChild(recruitSlingerButton);
// Defender recruitment button
var recruitDefenderButton = new Button('Recruit Defender (25g)', {
width: 400,
height: 100,
textSize: 40,
bgColor: 0x404040
});
recruitDefenderButton.x = 0; // Center position
recruitDefenderButton.y = 80; // Same row as other recruitment buttons
buttonContainer.addChild(recruitDefenderButton);
// Research Buttons
var researchMilitiaButton = new Button('Research Rookie (50g)', {
width: 400,
height: 100,
textSize: 40,
bgColor: 0x004080 // Blueish button
});
researchMilitiaButton.x = -220; // Position left, above recruitment
researchMilitiaButton.y = -220; // Position above recruitment buttons
buttonContainer.addChild(researchMilitiaButton);
var researchSlingerButton = new Button('Research Trainee (75g)', {
width: 400,
height: 100,
textSize: 40,
bgColor: 0x004080 // Blueish button
});
researchSlingerButton.x = 220; // Position right, above recruitment
researchSlingerButton.y = -220; // Position above recruitment buttons
buttonContainer.addChild(researchSlingerButton);
// Tier 2 Research Buttons (Initially hidden)
var researchSwordsmanButton = new Button('Research Swordsman (' + SWORDSMAN_RESEARCH_COST + 'g)', {
width: 400,
height: 100,
textSize: 35,
// Slightly smaller text if needed
bgColor: 0x800080 // Purpleish button
});
researchSwordsmanButton.x = -220;
researchSwordsmanButton.y = -350; // Position above Tier 1 research
researchSwordsmanButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchSwordsmanButton);
var researchArcherButton = new Button('Research Archer (' + ARCHER_RESEARCH_COST + 'g)', {
width: 400,
height: 100,
textSize: 35,
// Slightly smaller text if needed
bgColor: 0x800080 // Purpleish button
});
researchArcherButton.x = 220;
researchArcherButton.y = -350; // Position above Tier 1 research
researchArcherButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchArcherButton);
buttonContainer.addChild(researchArcherButton);
// Defender Research Buttons
var researchShieldedButton = new Button('Research Shielded (' + SHIELDED_RESEARCH_COST + 'g)', {
width: 400,
height: 100,
textSize: 35,
bgColor: 0x6A5ACD // SlateBlue button
});
researchShieldedButton.x = 0; // Position center, above Tier 1 Melee/Ranged
researchShieldedButton.y = -220; // Same level as Tier 1 Melee/Ranged research
buttonContainer.addChild(researchShieldedButton);
var researchHopliteButton = new Button('Research Hoplite (' + HOPLITE_RESEARCH_COST + 'g)', {
width: 400,
height: 100,
textSize: 35,
bgColor: 0x483D8B // DarkSlateBlue button
});
researchHopliteButton.x = 0; // Position center, above Tier 2 Melee/Ranged
researchHopliteButton.y = -350; // Same level as Tier 2 Melee/Ranged research
researchHopliteButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchHopliteButton);
// Adjust positions to accommodate the third column (Defender research)
// Tier 1 Research
researchMilitiaButton.x = -440; // Move left
researchMilitiaButton.y = -220;
researchSlingerButton.x = 440; // Move right
researchSlingerButton.y = -220;
researchShieldedButton.x = 0; // Center
researchShieldedButton.y = -220;
// Tier 2 Research
researchSwordsmanButton.x = -440; // Move left
researchSwordsmanButton.y = -350;
researchArcherButton.x = 440; // Move right
researchArcherButton.y = -350;
researchHopliteButton.x = 0; // Center
researchHopliteButton.y = -350;
// Adjust Start Button position further up
startButton.y = -480; // Move start button further up
// Tier 3 Research Buttons (Initially hidden)
var researchEliteSwordsmanButton = new Button('Research Elite (' + ELITESWORDSMAN_RESEARCH_COST + 'g)', {
width: 400,
height: 100,
textSize: 35,
bgColor: 0xFF4500 // Orangish-Red button
});
researchEliteSwordsmanButton.x = -440; // Position left, above Tier 2
researchEliteSwordsmanButton.y = -480; // Position above Tier 2 research
researchEliteSwordsmanButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchEliteSwordsmanButton);
var researchCrossbowmanButton = new Button('Research Crossbow (' + CROSSBOWMAN_RESEARCH_COST + 'g)', {
width: 400,
height: 100,
textSize: 35,
bgColor: 0xFF4500 // Orangish-Red button
});
researchCrossbowmanButton.x = 440; // Position right, above Tier 2
researchCrossbowmanButton.y = -480; // Position above Tier 2 research
researchCrossbowmanButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchCrossbowmanButton);
var researchDarkKnightButton = new Button('Research D.Knight (' + DARKKNIGHT_RESEARCH_COST + 'g)', {
width: 400,
height: 100,
textSize: 35,
bgColor: 0x8B0000 // DarkRed button
});
researchDarkKnightButton.x = 0; // Position center, above Tier 2
researchDarkKnightButton.y = -480; // Position above Tier 2 research
researchDarkKnightButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchDarkKnightButton);
// Adjust Start Button position even further up to accommodate Tier 3 research
startButton.y = -610; // Move start button further up
// --- Artillery Research Buttons ---
var researchBomberButton = new Button('Research Bomber (' + BOMBER_RESEARCH_COST + 'g)', {
width: 380,
// Slightly narrower for 4 columns
height: 100,
textSize: 35,
bgColor: 0x8A2BE2 // BlueViolet
});
researchBomberButton.x = 220; // Position between Defender and Ranged (Right side)
researchBomberButton.y = -220; // Tier 1 row
researchBomberButton.visible = true; // Initially visible
buttonContainer.addChild(researchBomberButton);
var researchCatapultButton = new Button('Research Catapult (' + CATAPULT_RESEARCH_COST + 'g)', {
width: 380,
height: 100,
textSize: 35,
bgColor: 0x9932CC // DarkOrchid
});
researchCatapultButton.x = 220; // Position between Defender and Ranged (Right side)
researchCatapultButton.y = -350; // Tier 2 row
researchCatapultButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchCatapultButton);
var researchDragonButton = new Button('Research Dragon (' + DRAGON_RESEARCH_COST + 'g)', {
width: 380,
height: 100,
textSize: 35,
bgColor: 0xBA55D3 // MediumOrchid
});
researchDragonButton.x = 220; // Position between Defender and Ranged (Right side)
researchDragonButton.y = -480; // Tier 3 row
researchDragonButton.visible = false; // Hidden until prerequisite met
buttonContainer.addChild(researchDragonButton);
// --- Adjust existing button positions for 4 columns ---
var buttonXOffsetLarge = 660; // For outer buttons (Melee, Ranged)
var buttonXOffsetSmall = 220; // For inner buttons (Defender, Artillery)
// Research Buttons
researchMilitiaButton.x = -buttonXOffsetLarge; // Far Left
researchMilitiaButton.width = 380;
researchShieldedButton.x = -buttonXOffsetSmall; // Mid Left // Fix: Used correct variable name
researchShieldedButton.width = 380; // Fix: Used correct variable name
researchBomberButton.x = buttonXOffsetSmall; // Mid Right (new)
researchBomberButton.width = 380;
researchSlingerButton.x = buttonXOffsetLarge; // Far Right
researchSlingerButton.width = 380;
researchSwordsmanButton.x = -buttonXOffsetLarge; // Far Left
researchSwordsmanButton.width = 380;
researchHopliteButton.x = -buttonXOffsetSmall; // Mid Left
researchHopliteButton.width = 380;
researchCatapultButton.x = buttonXOffsetSmall; // Mid Right (new)
researchCatapultButton.width = 380;
researchArcherButton.x = buttonXOffsetLarge; // Far Right
researchArcherButton.width = 380;
researchEliteSwordsmanButton.x = -buttonXOffsetLarge; // Far Left
researchEliteSwordsmanButton.width = 380;
researchDarkKnightButton.x = -buttonXOffsetSmall; // Mid Left
researchDarkKnightButton.width = 380;
researchDragonButton.x = buttonXOffsetSmall; // Mid Right (new)
researchDragonButton.width = 380;
researchCrossbowmanButton.x = buttonXOffsetLarge; // Far Right
researchCrossbowmanButton.width = 380;
// Recruitment Buttons
recruitMilitiaButton.x = -buttonXOffsetLarge; // Far Left
recruitMilitiaButton.width = 380;
recruitDefenderButton.x = -buttonXOffsetSmall; // Mid Left
recruitDefenderButton.width = 380;
// recruitArtilleryButton needs to be created first, then positioned
recruitSlingerButton.x = buttonXOffsetLarge; // Far Right
recruitSlingerButton.width = 380;
// Adjust Start Button position further up
startButton.y = -610; // Keep this adjustment
// --- Add Artillery Recruitment Button ---
var recruitArtilleryButton = new Button('Research Required', {
// Initial text
width: 380,
// Match other buttons
height: 100,
textSize: 35,
// Match research buttons
bgColor: 0x404040
});
recruitArtilleryButton.x = buttonXOffsetSmall; // Position Mid Right
recruitArtilleryButton.y = 80; // Same row as other recruitment buttons
recruitArtilleryButton.setEnabled(false); // Initially disabled
buttonContainer.addChild(recruitArtilleryButton);
// --- Game Logic Functions ---
function updateGoldDisplay() {
goldTxt.setText('Gold: ' + gold);
var canAffordMilitiaResearch = gold >= MILITIA_RESEARCH_COST;
var canAffordSlingerResearch = gold >= SLINGER_RESEARCH_COST;
var canAffordShieldedResearch = gold >= SHIELDED_RESEARCH_COST; // New Defender cost check
var canAffordSwordsmanResearch = gold >= SWORDSMAN_RESEARCH_COST;
var canAffordArcherResearch = gold >= ARCHER_RESEARCH_COST;
var canAffordHopliteResearch = gold >= HOPLITE_RESEARCH_COST;
var canAffordEliteSwordsmanResearch = gold >= ELITESWORDSMAN_RESEARCH_COST;
var canAffordCrossbowmanResearch = gold >= CROSSBOWMAN_RESEARCH_COST;
var canAffordDarkKnightResearch = gold >= DARKKNIGHT_RESEARCH_COST;
// Artillery research cost checks
var canAffordBomberResearch = gold >= BOMBER_RESEARCH_COST;
var canAffordCatapultResearch = gold >= CATAPULT_RESEARCH_COST;
var canAffordDragonResearch = gold >= DRAGON_RESEARCH_COST;
// --- Update Research Buttons ---
// Tier 1: Militia -> Rookie
if (militiaResearchComplete) {
researchMilitiaButton.visible = false; // Hide Tier 1 when complete
researchMilitiaButton.setEnabled(false);
} else {
researchMilitiaButton.visible = true;
researchMilitiaButton.setEnabled(canAffordMilitiaResearch && !isBattling);
}
// Tier 1: Slinger -> Trainee
if (slingerResearchComplete) {
researchSlingerButton.visible = false; // Hide Tier 1 when complete
researchSlingerButton.setEnabled(false);
} else {
researchSlingerButton.visible = true;
researchSlingerButton.setEnabled(canAffordSlingerResearch && !isBattling);
}
// Tier 1: Defender -> Shielded Skeleton
if (shieldedResearchComplete) {
researchShieldedButton.visible = false;
researchShieldedButton.setEnabled(false);
} else {
researchShieldedButton.visible = true;
researchShieldedButton.setEnabled(canAffordShieldedResearch && !isBattling);
}
// Tier 2: Rookie -> Swordsman
if (swordsmanResearchComplete) {
researchSwordsmanButton.visible = false; // Hide Tier 2 when complete
researchSwordsmanButton.setEnabled(false);
} else {
// Show Tier 2 only if Tier 1 is complete
researchSwordsmanButton.visible = militiaResearchComplete;
researchSwordsmanButton.setEnabled(militiaResearchComplete && canAffordSwordsmanResearch && !isBattling);
}
// Tier 2: Trainee -> Archer
if (archerResearchComplete) {
researchArcherButton.visible = false; // Hide Tier 2 when complete
researchArcherButton.setEnabled(false);
} else {
// Show Tier 2 only if Tier 1 is complete
researchArcherButton.visible = slingerResearchComplete;
researchArcherButton.setEnabled(slingerResearchComplete && canAffordArcherResearch && !isBattling);
}
// Tier 2: Shielded -> Hoplite
if (hopliteResearchComplete) {
researchHopliteButton.visible = false;
researchHopliteButton.setEnabled(false);
} else {
// Show Tier 2 only if Tier 1 is complete
researchHopliteButton.visible = shieldedResearchComplete;
researchHopliteButton.setEnabled(shieldedResearchComplete && canAffordHopliteResearch && !isBattling);
}
// Tier 3: Swordsman -> Elite Swordsman
if (eliteSwordsmanResearchComplete) {
researchEliteSwordsmanButton.visible = false;
researchEliteSwordsmanButton.setEnabled(false);
} else {
// Show Tier 3 only if Tier 2 is complete
researchEliteSwordsmanButton.visible = swordsmanResearchComplete;
researchEliteSwordsmanButton.setEnabled(swordsmanResearchComplete && canAffordEliteSwordsmanResearch && !isBattling);
}
// Tier 3: Archer -> Crossbowman
if (crossbowmanResearchComplete) {
researchCrossbowmanButton.visible = false;
researchCrossbowmanButton.setEnabled(false);
} else {
// Show Tier 3 only if Tier 2 is complete
researchCrossbowmanButton.visible = archerResearchComplete;
researchCrossbowmanButton.setEnabled(archerResearchComplete && canAffordCrossbowmanResearch && !isBattling);
}
// Tier 3: Hoplite -> Dark Knight
if (darkKnightResearchComplete) {
researchDarkKnightButton.visible = false;
researchDarkKnightButton.setEnabled(false);
} else {
// Show Tier 3 only if Tier 2 is complete
researchDarkKnightButton.visible = hopliteResearchComplete;
researchDarkKnightButton.setEnabled(hopliteResearchComplete && canAffordDarkKnightResearch && !isBattling);
}
// Tier 1: Artillery -> Bomber
if (bomberResearchComplete) {
researchBomberButton.visible = false;
researchBomberButton.setEnabled(false);
} else {
researchBomberButton.visible = true;
researchBomberButton.setEnabled(canAffordBomberResearch && !isBattling);
}
// Tier 2: Bomber -> Catapult
if (catapultResearchComplete) {
researchCatapultButton.visible = false;
researchCatapultButton.setEnabled(false);
} else {
// Show Tier 2 only if Tier 1 is complete
researchCatapultButton.visible = bomberResearchComplete;
researchCatapultButton.setEnabled(bomberResearchComplete && canAffordCatapultResearch && !isBattling);
}
// Tier 3: Catapult -> Dragon
if (dragonResearchComplete) {
researchDragonButton.visible = false;
researchDragonButton.setEnabled(false);
} else {
// Show Tier 3 only if Tier 2 is complete
researchDragonButton.visible = catapultResearchComplete;
researchDragonButton.setEnabled(catapultResearchComplete && canAffordDragonResearch && !isBattling);
}
// --- Update Recruitment Buttons ---
// Determine current highest researched MELEE unit
var currentMilitiaType = 'skeletonMilitia';
var militiaButtonText = 'Recruit Militia';
if (eliteSwordsmanResearchComplete) {
currentMilitiaType = 'skeletonEliteswordsman';
militiaButtonText = 'Recruit Elite';
} else if (swordsmanResearchComplete) {
currentMilitiaType = 'skeletonSwordsman';
militiaButtonText = 'Recruit Swordsman';
} else if (militiaResearchComplete) {
currentMilitiaType = 'skeletonRookie';
militiaButtonText = 'Recruit Rookie';
}
var currentMilitiaStats = UNIT_STATS[currentMilitiaType];
recruitMilitiaButton.setText(militiaButtonText + ' (' + currentMilitiaStats.cost + 'g)');
recruitMilitiaButton.setEnabled(gold >= currentMilitiaStats.cost && !isBattling);
// Determine current highest researched RANGED unit
var currentSlingerType = 'skeletonSlinger';
var slingerButtonText = 'Recruit Slinger';
if (crossbowmanResearchComplete) {
currentSlingerType = 'skeletonCrossbowman';
slingerButtonText = 'Recruit Crossbow';
} else if (archerResearchComplete) {
currentSlingerType = 'skeletonArcher';
slingerButtonText = 'Recruit Archer';
} else if (slingerResearchComplete) {
currentSlingerType = 'skeletonTraineeArcher';
slingerButtonText = 'Recruit Trainee';
}
var currentSlingerStats = UNIT_STATS[currentSlingerType];
recruitSlingerButton.setText(slingerButtonText + ' (' + currentSlingerStats.cost + 'g)');
recruitSlingerButton.setEnabled(gold >= currentSlingerStats.cost && !isBattling);
// Determine current highest researched DEFENDER unit
var currentDefenderType = null;
var defenderButtonText = 'Research Required';
if (darkKnightResearchComplete) {
currentDefenderType = 'skeletonDarkKnight';
defenderButtonText = 'Recruit D.Knight';
} else if (hopliteResearchComplete) {
currentDefenderType = 'skeletonHoplite';
defenderButtonText = 'Recruit Hoplite';
} else if (shieldedResearchComplete) {
currentDefenderType = 'skeletonShielded';
defenderButtonText = 'Recruit Shielded';
}
// Update defender button state based on research status
if (currentDefenderType) {
var currentDefenderStats = UNIT_STATS[currentDefenderType];
recruitDefenderButton.setText(defenderButtonText + ' (' + currentDefenderStats.cost + 'g)');
recruitDefenderButton.setEnabled(gold >= currentDefenderStats.cost && !isBattling);
} else {
// No defender research completed yet
recruitDefenderButton.setText('Research Required');
recruitDefenderButton.setEnabled(false);
}
// Determine current highest researched ARTILLERY unit
var currentArtilleryType = null;
var artilleryButtonText = 'Research Required';
if (dragonResearchComplete) {
currentArtilleryType = 'skeletonDragon';
artilleryButtonText = 'Recruit Dragon';
} else if (catapultResearchComplete) {
currentArtilleryType = 'skeletonPumpkinCatapult';
artilleryButtonText = 'Recruit Catapult';
} else if (bomberResearchComplete) {
currentArtilleryType = 'skeletonBomber';
artilleryButtonText = 'Recruit Bomber';
}
// Update artillery button state based on research status
if (currentArtilleryType) {
var currentArtilleryStats = UNIT_STATS[currentArtilleryType];
recruitArtilleryButton.setText(artilleryButtonText + ' (' + currentArtilleryStats.cost + 'g)');
recruitArtilleryButton.setEnabled(gold >= currentArtilleryStats.cost && !isBattling);
} else {
// No artillery research completed yet
recruitArtilleryButton.setText('Research Required');
recruitArtilleryButton.setEnabled(false); // Ensure it's disabled
}
}
function recruitUnit(unitType) {
if (isBattling) {
return;
} // Cannot recruit during battle
var stats = UNIT_STATS[unitType];
if (!stats) {
console.error("Attempted to recruit unknown unit type:", unitType);
return;
}
if (gold >= stats.cost) {
gold -= stats.cost;
try {
// Defensive coding for sound playback
LK.getSound('recruit').play();
} catch (e) {
console.log("Sound error playing recruit: " + e);
}
// Safely increment the count in the roster
playerArmyRoster[unitType] = (playerArmyRoster[unitType] || 0) + 1;
console.log("Recruited " + unitType + ". Roster:", playerArmyRoster);
updateGoldDisplay(); // Update UI after recruitment
} else {
console.log("Not enough gold to recruit " + unitType);
// Optional: Show feedback to the player (e.g., flashing the gold red)
LK.effects.flashObject(goldTxt, 0xFF0000, 300);
}
}
function researchUnit(researchType) {
if (isBattling) {
return;
} // Cannot research during battle
var cost = 0;
var cost = 0;
var researchName = '';
var canResearch = false;
var setResearchFlag = null; // Function to set the correct flag
if (researchType === 'militia' && !militiaResearchComplete) {
cost = MILITIA_RESEARCH_COST;
researchName = 'Rookie';
canResearch = true; // No prerequisite
setResearchFlag = function setResearchFlag() {
militiaResearchComplete = true;
};
} else if (researchType === 'slinger' && !slingerResearchComplete) {
cost = SLINGER_RESEARCH_COST;
researchName = 'Trainee Archer';
canResearch = true; // No prerequisite
setResearchFlag = function setResearchFlag() {
slingerResearchComplete = true;
};
} else if (researchType === 'swordsman' && !swordsmanResearchComplete) {
cost = SWORDSMAN_RESEARCH_COST;
researchName = 'Swordsman';
canResearch = militiaResearchComplete; // Prerequisite: Rookie researched
setResearchFlag = function setResearchFlag() {
swordsmanResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Rookie first.");
}
} else if (researchType === 'archer' && !archerResearchComplete) {
cost = ARCHER_RESEARCH_COST;
researchName = 'Archer';
canResearch = slingerResearchComplete; // Prerequisite: Trainee researched
setResearchFlag = function setResearchFlag() {
archerResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Trainee Archer first.");
}
} else if (researchType === 'shielded' && !shieldedResearchComplete) {
// New Defender Tier 1
cost = SHIELDED_RESEARCH_COST;
researchName = 'Shielded Skeleton';
canResearch = true; // No prerequisite
setResearchFlag = function setResearchFlag() {
shieldedResearchComplete = true;
};
} else if (researchType === 'hoplite' && !hopliteResearchComplete) {
// New Defender Tier 2
cost = HOPLITE_RESEARCH_COST;
researchName = 'Skeleton Hoplite';
canResearch = shieldedResearchComplete; // Prerequisite: Shielded researched
setResearchFlag = function setResearchFlag() {
hopliteResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Shielded Skeleton first.");
}
} else if (researchType === 'eliteswordsman' && !eliteSwordsmanResearchComplete) {
// Tier 3 Melee
cost = ELITESWORDSMAN_RESEARCH_COST;
researchName = 'Elite Swordsman';
canResearch = swordsmanResearchComplete; // Prerequisite: Swordsman researched
setResearchFlag = function setResearchFlag() {
eliteSwordsmanResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Swordsman first.");
}
} else if (researchType === 'crossbowman' && !crossbowmanResearchComplete) {
// Tier 3 Ranged
cost = CROSSBOWMAN_RESEARCH_COST;
researchName = 'Crossbowman';
canResearch = archerResearchComplete; // Prerequisite: Archer researched
setResearchFlag = function setResearchFlag() {
crossbowmanResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Archer first.");
}
} else if (researchType === 'darkknight' && !darkKnightResearchComplete) {
// Tier 3 Defender
cost = DARKKNIGHT_RESEARCH_COST;
researchName = 'Dark Knight';
canResearch = hopliteResearchComplete; // Prerequisite: Hoplite researched
setResearchFlag = function setResearchFlag() {
darkKnightResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Hoplite first.");
}
} else if (researchType === 'bomber' && !bomberResearchComplete) {
// Artillery Tier 1
cost = BOMBER_RESEARCH_COST;
researchName = 'Skeleton Bomber';
canResearch = true; // No prerequisite
setResearchFlag = function setResearchFlag() {
bomberResearchComplete = true;
};
} else if (researchType === 'catapult' && !catapultResearchComplete) {
// Artillery Tier 2
cost = CATAPULT_RESEARCH_COST;
researchName = 'Pumpkin Catapult';
canResearch = bomberResearchComplete; // Prerequisite: Bomber
setResearchFlag = function setResearchFlag() {
catapultResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Bomber first.");
}
} else if (researchType === 'dragon' && !dragonResearchComplete) {
// Artillery Tier 3
cost = DRAGON_RESEARCH_COST;
researchName = 'Skeleton Dragon';
canResearch = catapultResearchComplete; // Prerequisite: Catapult
setResearchFlag = function setResearchFlag() {
dragonResearchComplete = true;
};
if (!canResearch) {
console.log("Prerequisite not met: Research Catapult first.");
}
} else {
console.log("Research already complete, prerequisite not met, or invalid type:", researchType);
return; // Invalid type, already researched, or prereq not met
}
if (canResearch && gold >= cost) {
gold -= cost;
setResearchFlag(); // Set the specific research flag to true
try {
// Reuse upgrade sound, or create a specific 'research' sound
LK.getSound('upgradeUnit').play(); // Assuming 'upgradeUnit' sound is suitable
} catch (e) {
console.log("Sound error playing research sound: " + e);
}
console.log("Research complete for:", researchName);
updateGoldDisplay(); // Update UI immediately
} else {
console.log("Not enough gold to research " + researchName);
LK.effects.flashObject(goldTxt, 0xFF0000, 300);
}
}
function clearBattlefield() {
// Remove all existing units from the game and arrays
for (var i = playerUnits.length - 1; i >= 0; i--) {
if (playerUnits[i] && playerUnits[i].destroy) {
// Check if destroy exists
playerUnits[i].destroy();
}
}
playerUnits = []; // Reset array
for (var i = enemyUnits.length - 1; i >= 0; i--) {
if (enemyUnits[i] && enemyUnits[i].destroy) {
// Check if destroy exists
enemyUnits[i].destroy();
}
}
enemyUnits = []; // Reset array
// Remove the golden piggy if it exists
if (goldenPiggyInstance !== null) {
goldenPiggyInstance.destroy();
goldenPiggyInstance = null;
}
}
function spawnUnit(unitType, isEnemy) {
var stats = UNIT_STATS[unitType];
if (!stats) {
console.error("Cannot spawn unknown unit type:", unitType);
return null;
}
// Prepare config for the Unit class constructor
var config = {
isEnemy: isEnemy,
hp: stats.hp,
attackPower: stats.attackPower,
attackRange: stats.attackRange,
speed: stats.speed,
assetId: stats.assetId,
attackCooldownTime: stats.attackCooldownTime,
attackSoundId: stats.attackSoundId,
unitType: unitType // Pass the unit type
};
// Create new instance of Unit
var unit = new Unit(config);
// Determine spawn side and array
var armyArray = isEnemy ? enemyUnits : playerUnits;
var spawnX = isEnemy ? ENEMY_SPAWN_X : PLAYER_SPAWN_X;
// Calculate staggered Y position
var currentY = SPAWN_Y_START + armyArray.length * SPAWN_Y_STEP;
// Wrap Y position if it goes beyond the designated spawn area end
if (currentY > SPAWN_Y_END) {
var yRange = SPAWN_Y_END - SPAWN_Y_START;
if (yRange <= 0) {
yRange = SPAWN_Y_STEP;
} // Avoid division by zero or negative range
currentY = SPAWN_Y_START + armyArray.length * SPAWN_Y_STEP % yRange;
}
// Set initial position and state
unit.x = spawnX;
unit.y = currentY;
unit.lastX = unit.x; // Initialize last state correctly
// Add the unit to the game display and the logical array
game.addChild(unit);
armyArray.push(unit);
return unit; // Return the created unit if needed elsewhere
}
function startBattle() {
if (isBattling) {
return;
} // Prevent starting multiple battles
console.log("Starting battle...");
isBattling = true;
clearBattlefield(); // Remove any units from a previous battle
// Define enemy forces for this battle (can be made dynamic later)
if (Object.keys(currentEnemyConfig).length === 0) {
// Only initialize on first battle
currentEnemyConfig = {
humanFarmer: 3,
humanSlinger: 1
};
} // Otherwise use the already incremented config from previous win
// Spawn Player Units based on the current roster
console.log("Spawning player units:", playerArmyRoster);
for (var unitType in playerArmyRoster) {
if (playerArmyRoster.hasOwnProperty(unitType)) {
// Good practice for iterating objects
var count = playerArmyRoster[unitType];
for (var i = 0; i < count; i++) {
spawnUnit(unitType, false); // Spawn player unit (not enemy)
}
}
}
// Spawn Enemy Units based on the current configuration
console.log("Spawning enemy units:", currentEnemyConfig);
for (var unitType in currentEnemyConfig) {
if (currentEnemyConfig.hasOwnProperty(unitType)) {
var count = currentEnemyConfig[unitType];
for (var i = 0; i < count; i++) {
spawnUnit(unitType, true); // Spawn enemy unit
}
}
}
// Hide UI elements not relevant during battle
buttonContainer.visible = false;
// Ensure units arrays are populated before the first update runs
console.log("Battle started. Player units:", playerUnits.length, "Enemy units:", enemyUnits.length);
}
function endBattle(playerWon) {
// Check if the battle hasn't already ended to prevent multiple triggers
if (!isBattling) {
return;
}
console.log("Battle ended. Player won:", playerWon);
isBattling = false; // Set state before doing anything else
// Explicitly remove piggy when battle ends, BEFORE showing UI again.
// This ensures it's properly cleaned up and allows a new one to spawn in subsequent battles.
if (goldenPiggyInstance !== null) {
goldenPiggyInstance.destroy();
goldenPiggyInstance = null;
}
// Show UI elements again
buttonContainer.visible = true;
if (playerWon) {
var goldEarned = 50; // Example gold reward
gold += goldEarned;
try {
// Defensive coding for sound playback
LK.getSound('battleWon').play();
} catch (e) {
console.log("Sound error playing battleWon: " + e);
}
console.log("Earned " + goldEarned + " gold. Total gold:", gold);
// Update display immediately after awarding gold
updateGoldDisplay();
battleCounter++; // Increment the battle counter on win
villageTxt.setText('Village: ' + (battleCounter + 1)); // Update the village counter display
console.log("Battle won! Proceeding to village: " + (battleCounter + 1));
// Humans gain more units when player wins battles
for (var unitType in currentEnemyConfig) {
if (currentEnemyConfig.hasOwnProperty(unitType)) {
// Increase each existing unit type by 1-2 for the next battle
currentEnemyConfig[unitType] += Math.floor(Math.random() * 2) + 1;
}
}
// Introduce new units starting from the 5th battle (after winning the 4th)
if (battleCounter >= 4) {
// 4 wins means the next battle is the 5th
// Add Guard if not present, or increase count
if (!currentEnemyConfig.hasOwnProperty('humanGuard')) {
currentEnemyConfig['humanGuard'] = 1; // Start with 1
} else {
currentEnemyConfig['humanGuard'] += 1; // Add one more each subsequent win
}
// Add Militia Archer if not present, or increase count
if (!currentEnemyConfig.hasOwnProperty('humanMilitiaArcher')) {
currentEnemyConfig['humanMilitiaArcher'] = 1; // Start with 1
} else {
currentEnemyConfig['humanMilitiaArcher'] += 1; // Add one more each subsequent win
}
console.log("Stronger human reinforcements (Guards, Militia Archers) have arrived!");
}
// Introduce Local Protector starting from the 6th battle (after winning the 5th)
if (battleCounter >= 5) {
// 5 wins means the next battle is the 6th
if (!currentEnemyConfig.hasOwnProperty('humanLocalprotector')) {
currentEnemyConfig['humanLocalprotector'] = 1; // Start with 1
} else {
currentEnemyConfig['humanLocalprotector'] += 1; // Add one more each win
}
console.log("Local Protector reinforcements have arrived!");
}
// Introduce elite units starting from the 8th battle (after winning the 7th)
if (battleCounter >= 7) {
// 7 wins means the next battle is the 8th
// Add Elite Guard if not present, or increase count
if (!currentEnemyConfig.hasOwnProperty('humanEliteGuard')) {
currentEnemyConfig['humanEliteGuard'] = 1; // Start with 1
} else {
currentEnemyConfig['humanEliteGuard'] += 1; // Add one more each subsequent win
}
// Add Archer if not present, or increase count
if (!currentEnemyConfig.hasOwnProperty('humanArcher')) {
currentEnemyConfig['humanArcher'] = 1; // Start with 1
} else {
currentEnemyConfig['humanArcher'] += 1; // Add one more each subsequent win
}
console.log("Elite human reinforcements (Elite Guards, Archers) have arrived!");
}
// Introduce Guardian starting from the 9th battle (after winning the 8th)
if (battleCounter >= 8) {
// 8 wins means the next battle is the 9th
if (!currentEnemyConfig.hasOwnProperty('humanGuardian')) {
currentEnemyConfig['humanGuardian'] = 1; // Start with 1
} else {
currentEnemyConfig['humanGuardian'] += 1; // Add one more each win
}
console.log("Guardian reinforcements have arrived!");
}
// Introduce Bomber starting from the 13th battle (after winning the 12th)
if (battleCounter >= 12) {
if (!currentEnemyConfig.hasOwnProperty('Bomber')) {
currentEnemyConfig['Bomber'] = 1;
} else {
currentEnemyConfig['Bomber'] += 1;
}
console.log("Bomber reinforcements have arrived!");
}
// Introduce King's Guard starting from the 15th battle (after winning the 14th)
if (battleCounter >= 14) {
if (!currentEnemyConfig.hasOwnProperty("King's guard")) {
currentEnemyConfig["King's guard"] = 1;
} else {
currentEnemyConfig["King's guard"] += 1;
}
console.log("King's Guard reinforcements have arrived!");
}
// Introduce Crossbowman starting from the 16th battle (after winning the 15th)
if (battleCounter >= 15) {
if (!currentEnemyConfig.hasOwnProperty("Crossbowman")) {
currentEnemyConfig["Crossbowman"] = 1;
} else {
currentEnemyConfig["Crossbowman"] += 1;
}
console.log("Crossbowman reinforcements have arrived!");
}
// Introduce Cannoneer starting from the 20th battle (after winning the 19th)
if (battleCounter >= 19) {
if (!currentEnemyConfig.hasOwnProperty('Cannoneer')) {
currentEnemyConfig['Cannoneer'] = 1;
} else {
currentEnemyConfig['Cannoneer'] += 1;
}
console.log("Cannoneer reinforcements have arrived!");
}
// Introduce King's Shield starting from the 23rd battle (after winning the 22nd)
if (battleCounter >= 22) {
if (!currentEnemyConfig.hasOwnProperty("King's shield")) {
currentEnemyConfig["King's shield"] = 1;
} else {
currentEnemyConfig["King's shield"] += 1;
}
console.log("King's Shield reinforcements have arrived!");
}
// Introduce Trebuchet starting from the 26th battle (after winning the 25th)
if (battleCounter >= 25) {
if (!currentEnemyConfig.hasOwnProperty('Trebuchet')) {
currentEnemyConfig['Trebuchet'] = 1;
} else {
currentEnemyConfig['Trebuchet'] += 1;
}
console.log("Trebuchet reinforcements have arrived!");
}
console.log("Enemy forces have grown stronger for the next battle:", currentEnemyConfig);
// Game continues, player can choose to recruit or battle again
} else {
console.log("Player lost the battle.");
// Game over is handled by LK engine which resets the game state
LK.showGameOver();
// Note: Gold and roster will reset unless storage plugin is used.
}
// Update button states based on new gold amount AFTER battle outcome is decided
// This is especially important if the player lost, as LK.showGameOver might reset things
// before this line runs if not careful. updateGoldDisplay() called inside win condition is safer.
// If player loses, the game resets anyway, so updating buttons might not be needed here.
if (playerWon) {
updateGoldDisplay(); // Ensure buttons reflect new gold total after winning
}
}
// --- Initialize Game State ---
// Assign onClick handlers to buttons *after* they are created
startButton.onClick = startBattle; // Assign the function reference
recruitMilitiaButton.onClick = function () {
var unitToRecruit = 'skeletonMilitia'; // Default
if (eliteSwordsmanResearchComplete) {
unitToRecruit = 'skeletonEliteswordsman';
} else if (swordsmanResearchComplete) {
unitToRecruit = 'skeletonSwordsman';
} else if (militiaResearchComplete) {
unitToRecruit = 'skeletonRookie';
}
recruitUnit(unitToRecruit);
};
recruitSlingerButton.onClick = function () {
var unitToRecruit = 'skeletonSlinger'; // Default
if (crossbowmanResearchComplete) {
unitToRecruit = 'skeletonCrossbowman';
} else if (archerResearchComplete) {
unitToRecruit = 'skeletonArcher';
} else if (slingerResearchComplete) {
unitToRecruit = 'skeletonTraineeArcher';
}
recruitUnit(unitToRecruit);
};
recruitDefenderButton.onClick = function () {
// Default to no unit (research required)
var unitToRecruit = null;
// Check research status to determine which defender unit to recruit
if (darkKnightResearchComplete) {
unitToRecruit = 'skeletonDarkKnight';
} else if (hopliteResearchComplete) {
unitToRecruit = 'skeletonHoplite';
} else if (shieldedResearchComplete) {
unitToRecruit = 'skeletonShielded';
}
// Only attempt to recruit if a valid unit type is available
if (unitToRecruit) {
recruitUnit(unitToRecruit);
}
};
// Tier 1 Research Button Handlers
researchMilitiaButton.onClick = function () {
researchUnit('militia');
};
researchSlingerButton.onClick = function () {
researchUnit('slinger');
};
// Tier 2 Research Button Handlers
researchSwordsmanButton.onClick = function () {
researchUnit('swordsman');
};
researchArcherButton.onClick = function () {
researchUnit('archer');
};
// Defender Research Button Handlers
researchShieldedButton.onClick = function () {
researchUnit('shielded');
};
researchHopliteButton.onClick = function () {
researchUnit('hoplite');
};
// Tier 3 Research Button Handlers
researchEliteSwordsmanButton.onClick = function () {
researchUnit('eliteswordsman');
};
researchCrossbowmanButton.onClick = function () {
researchUnit('crossbowman');
};
researchDarkKnightButton.onClick = function () {
researchUnit('darkknight');
};
// Artillery Research Button Handlers
researchBomberButton.onClick = function () {
researchUnit('bomber');
};
researchCatapultButton.onClick = function () {
researchUnit('catapult');
};
researchDragonButton.onClick = function () {
researchUnit('dragon');
};
// Artillery Recruitment Button Handler
recruitArtilleryButton.onClick = function () {
var unitToRecruit = null;
if (dragonResearchComplete) {
unitToRecruit = 'skeletonDragon';
} else if (catapultResearchComplete) {
unitToRecruit = 'skeletonPumpkinCatapult';
} else if (bomberResearchComplete) {
unitToRecruit = 'skeletonBomber';
}
// Only attempt to recruit if a valid unit type is available
if (unitToRecruit) {
recruitUnit(unitToRecruit);
}
};
updateGoldDisplay(); // Set initial gold text and button states based on starting gold (10) and research state
// --- Game Update Loop ---
// This function is called by the LK engine automatically 60 times per second.
game.update = function () {
if (!isBattling) {
// Game is idle, waiting for player action (recruit/start battle)
// No unit updates needed.
return;
}
// --- Battle Logic ---
// Update Player Units (Iterate backwards for safe removal)
for (var i = playerUnits.length - 1; i >= 0; i--) {
var pUnit = playerUnits[i];
// Ensure unit exists and has methods before calling them
if (!pUnit) {
continue;
}
if (pUnit.isDead) {
if (pUnit.destroy) {
pUnit.destroy();
} // Remove graphics from game
playerUnits.splice(i, 1); // Remove from logical array
continue; // Move to the next unit
}
// Pass the array of enemies for target finding
if (pUnit.update) {
pUnit.update(enemyUnits);
}
}
// Update Enemy Units (Iterate backwards for safe removal)
for (var i = enemyUnits.length - 1; i >= 0; i--) {
var eUnit = enemyUnits[i];
if (!eUnit) {
continue;
}
if (eUnit.isDead) {
if (eUnit.destroy) {
eUnit.destroy();
}
enemyUnits.splice(i, 1);
continue;
}
// Pass the array of player units for target finding
if (eUnit.update) {
eUnit.update(playerUnits);
}
}
// --- Golden Piggy Logic ---
// Chance to spawn a piggy if one isn't already active
if (isBattling && goldenPiggyInstance === null) {
// Low chance per tick, e.g., 1 in 1000 ticks on average
var PIGGY_SPAWN_CHANCE = 1 / (60 * 10); // Roughly once every 10 seconds average
if (Math.random() < PIGGY_SPAWN_CHANCE) {
console.log("Spawning Golden Piggy!");
goldenPiggyInstance = new GoldenPiggy();
// Spawn position (e.g., random y in the middle vertical third, center horizontally)
var spawnY = GAME_HEIGHT * 0.33 + Math.random() * (GAME_HEIGHT * 0.33);
goldenPiggyInstance.x = GAME_WIDTH / 2;
goldenPiggyInstance.y = spawnY;
game.addChild(goldenPiggyInstance); // Add to display
}
}
// Update active piggy and remove if collected/timed out
if (goldenPiggyInstance !== null) {
if (goldenPiggyInstance.isCollected) {
goldenPiggyInstance.destroy();
goldenPiggyInstance = null;
} else {
goldenPiggyInstance.update(); // Call piggy's own update logic
}
}
// Check Win/Loss Conditions *after* all units have been updated for the tick
// Only check if the battle is currently marked as active
if (isBattling) {
if (enemyUnits.length === 0 && playerUnits.length > 0) {
// Player wins: All enemies are defeated, player still has units.
endBattle(true);
} else if (playerUnits.length === 0) {
// Player loses: All player units are defeated. (Enemy count doesn't matter here)
// We might get here even if enemies also died in the same tick.
endBattle(false);
}
}
};
// --- Input Handling ---
// Button clicks are managed by the Button class instances via their `up` method.
// No global game-level mouse/touch handlers needed for this MVP's interaction model.
Skeleton with a wooden sword. In-Game asset. 2d. High contrast. No shadows
Skeleton facing left with a slingshot. In-Game asset. 2d. High contrast. No shadows
Farmer with a pitchfork. In-Game asset. 2d. High contrast. No shadows
Farmer with a slingshot. In-Game asset. 2d. High contrast. No shadows
Skeleton with copper sword, copper helmet and copper shield. In-Game asset. 2d. High contrast. No shadows
A skeleton with a bow. In-Game asset. 2d. High contrast. No shadows
Skeleton with Iron sword, iron helmet and a Reinforced wooden shield. In-Game asset. 2d. High contrast. No shadows
Skeleton with a reinforced bow and a hood. In-Game asset. 2d. High contrast. No shadows
Human with copper Armor, a copper sword and shield. In-Game asset. 2d. High contrast. No shadows
A human with a bow and a quiver. In-Game asset. 2d. High contrast. No shadows
Human with Iron Armor, iron sword and iron shield. In-Game asset. 2d. High contrast. No shadows
A human with a reinforced bow, iron helmet and a bucket full of arrows. In-Game asset. 2d. High contrast. No shadows
Skeleton holding a big copper shield with both hands. In-Game asset. 2d. High contrast. No shadows
Skeleton with an Iron helmet, Iron spear and a big iron shield. In-Game asset. 2d. High contrast. No shadows
A golden pig. In-Game asset. 2d. High contrast. No shadows
Human with a big iron shield. In-Game asset. 2d. High contrast. No shadows
Human with a big Copper shield. In-Game asset. 2d. High contrast. No shadows
Skeleton with a blue hoodie and cape, golden sword and a reinforced iron shield. In-Game asset. 2d. High contrast. No shadows
Skeleton with a dead wolf's head as a hood, a Crossbow and a big quiver. In-Game asset. 2d. High contrast. No shadows
Skeleton with it's skull covered with a dark helmet, a dark axe and a shield with a red skull in the middle. In-Game asset. 2d. High contrast. No shadows
Skeleton with a bomb. In-Game asset. 2d. High contrast. No shadows
Pumpkin catapult made out of bones. In-Game asset. 2d. High contrast. No shadows
Skeleton with a blue hoodie and cape riding a skeleton dragon. In-Game asset. 2d. High contrast. No shadows
A knight with golden armour, golden shield and golden sword. In-Game asset. 2d. High contrast. No shadows
A human with a golden helmet, crossbow and a big bucket full of arrows in his back. In-Game asset. 2d. High contrast. No shadows
Human with a BIG golden shield and shining reinforced golden armor. In-Game asset. 2d. High contrast. No shadows
Human with a bomb. In-Game asset. 2d. High contrast. No shadows
Cannon. In-Game asset. 2d. High contrast. No shadows
Trebuchet on wheels. In-Game asset. 2d. High contrast. No shadows