User prompt
Add new units: skeleton swordsman (melee); Skeleton archer (range)
User prompt
Add new human enemies: Guard of the village, Millitia archer; they start appearing on the fifth village
User prompt
Rookie uses skeletonRookie asset; meanwhile Trainee archer uses skeletonTraineearcher asset
User prompt
Upgrading is REMOVED! instead, it's replaced with research which changes the buttons to make them spawn the new and stronger units instead of the old units; researching uses gold
User prompt
Skeleton millitia turns into skeleton rookie
User prompt
The new skeletons use their respective assets!
User prompt
Now you can upgrade your units by tapping them! This will cost gold, obviously. Example of upgrading: Tap on millitia, it will turn into rookie [example]. Units (from weakest to stronghest): melee: Millitia, Rookie, Swordsman; Ranged: Slinger, Trainee archer, archer
User prompt
Here's how upgrading works: Tap on a millitia, it will turn into a rookie (Skeletons[from weakest to stronghest]: Melee: millitia, rookie, swordsman; Ranged: slinger, trainee archer, archer
User prompt
Now you can upgrade your units by tapping them! This will cost gold, obviously
User prompt
Now you gain more gold the more enemies you kill
User prompt
Humans gain more units the more you win
User prompt
You start out with 10 gold
User prompt
You start with a millitia
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'for (var i = 0; i < enemyArray.length; i++) {' Line Number: 86
Code edit (1 edits merged)
Please save this source code
User prompt
Skeleton War: Village Raid
Initial prompt
Skeleton war: a game where you are the leader of an army of Skeletons. You need to take revenge from humans that have destroyed your castle. You start with some weak skeletonsbbut, by destroying enemy villages, you get gold that can be used to buy more skeletons or upgrade your other skeletons (Skeletons(from weakest to stronghest): Melee: skeleton millitia, skeleton rookie, skeleton swordsman; Ranged: Skeleton slinger, skeleton trainee archer, skeleton archer; Defender: Skeleton spearman, skeleton holpite, skeleton conniesur; Melee cavaliry: Skeleton scout, light skeleton rider, skeleton rider) (Humans (enemies from weakest to stronghest): Melee: Farmer, swordsman; Ranged: Slinger, archer; Defender: shieldman, holpite; Melee cavaliry: Scout, light rider)
/**** * 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