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