User prompt
Add new units: skeleton swordsman (melee); Skeleton archer (range)
User prompt
Add new human enemies: Guard of the village, Millitia archer; they start appearing on the fifth village
User prompt
Rookie uses skeletonRookie asset; meanwhile Trainee archer uses skeletonTraineearcher asset
User prompt
Upgrading is REMOVED! instead, it's replaced with research which changes the buttons to make them spawn the new and stronger units instead of the old units; researching uses gold
User prompt
Skeleton millitia turns into skeleton rookie
User prompt
The new skeletons use their respective assets!
User prompt
Now you can upgrade your units by tapping them! This will cost gold, obviously. Example of upgrading: Tap on millitia, it will turn into rookie [example]. Units (from weakest to stronghest): melee: Millitia, Rookie, Swordsman; Ranged: Slinger, Trainee archer, archer
User prompt
Here's how upgrading works: Tap on a millitia, it will turn into a rookie (Skeletons[from weakest to stronghest]: Melee: millitia, rookie, swordsman; Ranged: slinger, trainee archer, archer
User prompt
Now you can upgrade your units by tapping them! This will cost gold, obviously
User prompt
Now you gain more gold the more enemies you kill
User prompt
Humans gain more units the more you win
User prompt
You start out with 10 gold
User prompt
You start with a millitia
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'for (var i = 0; i < enemyArray.length; i++) {' Line Number: 86
Code edit (1 edits merged)
Please save this source code
User prompt
Skeleton War: Village Raid
Initial prompt
Skeleton war: a game where you are the leader of an army of Skeletons. You need to take revenge from humans that have destroyed your castle. You start with some weak skeletonsbbut, by destroying enemy villages, you get gold that can be used to buy more skeletons or upgrade your other skeletons (Skeletons(from weakest to stronghest): Melee: skeleton millitia, skeleton rookie, skeleton swordsman; Ranged: Skeleton slinger, skeleton trainee archer, skeleton archer; Defender: Skeleton spearman, skeleton holpite, skeleton conniesur; Melee cavaliry: Skeleton scout, light skeleton rider, skeleton rider) (Humans (enemies from weakest to stronghest): Melee: Farmer, swordsman; Ranged: Slinger, archer; Defender: shieldman, holpite; Melee cavaliry: Scout, light rider)
/**** * Classes ****/ // 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; }); // 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 || enemy.isDead) { // Added check for enemy existence 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.graphics, 0xFF0000, 100); // Flash the graphics asset if (self.hp <= 0) { // Award gold if an enemy dies from player attack if (self.isEnemy) { // Award gold based on unit type/strength (consider using self.unitType later) gold += 5; // Base gold reward (TODO: Make this dynamic based on enemy type) 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; self.interactive = false; // Disable interaction when dead // It will be removed from the game and array in game.update } }; self.upgrade = function () { if (self.isEnemy || !self.upgradeTo || !isBattling || self.isDead) { console.log("Upgrade condition not met."); return; // Can only upgrade living player units during battle with a valid upgrade path } var upgradeStats = UNIT_STATS[self.upgradeTo]; if (!upgradeStats) { console.error("Cannot find stats for upgrade target:", self.upgradeTo); return; } var cost = self.upgradeCost; if (gold >= cost) { gold -= cost; updateGoldDisplay(); // Play sound (optional) try { LK.getSound('upgradeUnit').play(); } catch (e) { console.log("Sound error playing upgradeUnit: " + e); } console.log("Upgrading " + self.unitType + " to " + self.upgradeTo + " for " + cost + "g"); // --- Update Stats --- var oldMaxHp = self.maxHp; var currentHpRatio = self.hp / oldMaxHp; // Preserve HP percentage self.unitType = self.upgradeTo; // Update the type identifier self.attackPower = upgradeStats.attackPower; self.attackRange = upgradeStats.attackRange; self.speed = upgradeStats.speed; self.maxHp = upgradeStats.hp; self.hp = currentHpRatio * self.maxHp; // Scale current HP self.attackCooldownTime = upgradeStats.attackCooldownTime || 60; // Update cooldown time self.attackSoundId = upgradeStats.attackSoundId || 'attackMelee'; // Update sound // Update future upgrade path self.upgradeTo = upgradeStats.upgradeTo; self.upgradeCost = upgradeStats.upgradeCost; // --- Update Graphics (if asset changes) --- if (self.assetId !== upgradeStats.assetId) { self.assetId = upgradeStats.assetId; if (self.graphics) { self.removeChild(self.graphics); // Remove old graphics } // Attach new graphics self.graphics = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); // Add visual feedback for upgrade? LK.effects.flashObject(self.graphics, 0xFFFF00, 300); // Yellow flash } else { // Flash existing graphics if asset doesn't change LK.effects.flashObject(self.graphics, 0xFFFF00, 300); // Yellow flash } // If no further upgrade, disable interaction for upgrading? // Or just let the check `!self.upgradeTo` handle it. console.log("Upgrade complete. New type:", self.unitType); } else { console.log("Not enough gold to upgrade. Cost:", cost, "Have:", gold); // Optional: Flash the unit or gold display red LK.effects.flashObject(self.graphics, 0xFF0000, 200); } }; // 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 }; // --- Event Handlers --- // Called automatically by LK engine if interactive self.down = function (x, y, obj) { // Only allow upgrading player units during battle if (!self.isEnemy && isBattling && !self.isDead) { console.log("Player unit tapped:", self.unitType, " Gold:", gold, " Upgrade Cost:", self.upgradeCost); // Attempt upgrade immediately on tap if (self.upgradeTo && self.upgradeCost > 0) { self.upgrade(); } else { console.log("No upgrade available for this unit."); // Optional feedback: maybe a small visual effect? } } }; // --- Initialize --- var stats = UNIT_STATS[config.unitType]; // Get stats based on type if (!stats) { console.error("Unit creation failed: Invalid unitType", config.unitType); return null; // Prevent creation if stats are missing } self.unitType = config.unitType; // Store the specific type string self.isEnemy = config.isEnemy; self.hp = stats.hp; self.maxHp = stats.hp; // Store max HP self.attackPower = stats.attackPower; self.attackRange = stats.attackRange; self.attackCooldownTime = stats.attackCooldownTime || 60; // Default 1 sec cooldown self.speed = stats.speed; self.assetId = stats.assetId; self.attackSoundId = stats.attackSoundId || 'attackMelee'; // Upgrade properties (may be null/0 if no upgrade available) self.upgradeTo = stats.upgradeTo || null; self.upgradeCost = stats.upgradeCost || 0; // Attach graphics *after* properties are set self.graphics = self.attachAsset(self.assetId, { anchorX: 0.5, // Centered anchor anchorY: 0.5 // Centered anchor }); // 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 // Make units interactive so they can be tapped self.interactive = true; 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 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 // Unit stats lookup table // Unit stats lookup table, including upgrade info var UNIT_STATS = { // --- Player Skeletons --- skeletonMilitia: { hp: 50, attackPower: 5, attackRange: 80, // Melee range speed: 1.5, assetId: 'skeletonMilitia', cost: 10, // Recruitment cost attackSoundId: 'attackMelee', upgradeTo: 'skeletonRookie', // Next unit type upgradeCost: 20 // Gold cost to upgrade }, skeletonRookie: { // Placeholder stats for upgrade target hp: 75, attackPower: 8, attackRange: 85, speed: 1.6, assetId: 'skeletonMilitia', // Placeholder: Use same asset for now cost: 0, // Not directly recruitable attackSoundId: 'attackMelee', upgradeTo: null, // No further upgrade defined yet upgradeCost: 0 }, skeletonSlinger: { hp: 30, attackPower: 7, attackRange: 400, // Ranged speed: 1.2, assetId: 'skeletonSlinger', cost: 15, // Recruitment cost attackCooldownTime: 90, attackSoundId: 'attackRanged', upgradeTo: 'skeletonTraineeArcher', // Next unit type upgradeCost: 25 // Gold cost to upgrade }, skeletonTraineeArcher: { // Placeholder stats for upgrade target hp: 45, attackPower: 10, attackRange: 450, speed: 1.3, assetId: 'skeletonSlinger', // Placeholder: Use same asset for now cost: 0, // Not directly recruitable attackCooldownTime: 85, attackSoundId: 'attackRanged', upgradeTo: null, // No further upgrade defined yet upgradeCost: 0 }, // --- Human Enemies --- 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' } // Add other unit stats here as needed... }; // Add sounds for upgrade action (optional) // Game state variables - Defined globally for access within functions and update loop var gold = 10; var playerUnits = []; var enemyUnits = []; var isBattling = false; var currentEnemyConfig = {}; // Store the config for the current battle // 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); // 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 = -220; // Position left of center 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 = 220; // Position right of center recruitSlingerButton.y = 80; // Position below start button buttonContainer.addChild(recruitSlingerButton); // --- Game Logic Functions --- function updateGoldDisplay() { goldTxt.setText('Gold: ' + gold); // Update recruitment button enabled state and text var militiaCost = UNIT_STATS['skeletonMilitia'].cost; recruitMilitiaButton.setText('Recruit Militia (' + militiaCost + 'g)'); recruitMilitiaButton.setEnabled(gold >= militiaCost && !isBattling); var slingerCost = UNIT_STATS['skeletonSlinger'].cost; recruitSlingerButton.setText('Recruit Slinger (' + slingerCost + 'g)'); recruitSlingerButton.setEnabled(gold >= slingerCost && !isBattling); } 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 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 } 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 // Prepare config for the Unit class constructor var config = { unitType: unitType, // Pass the specific type string isEnemy: isEnemy // Stats are now looked up inside the Unit constructor based on unitType // hp: stats.hp, // No longer needed here // attackPower: stats.attackPower, // No longer needed here // attackRange: stats.attackRange, // No longer needed here // speed: stats.speed, // No longer needed here // assetId: stats.assetId, // No longer needed here // attackCooldownTime: stats.attackCooldownTime, // No longer needed here // attackSoundId: stats.attackSoundId // No longer needed here }; // 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 // 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(); // Humans gain more units when player wins battles for (var unitType in currentEnemyConfig) { if (currentEnemyConfig.hasOwnProperty(unitType)) { // Increase each unit type by 1-2 for the next battle currentEnemyConfig[unitType] += Math.floor(Math.random() * 2) + 1; } } 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 () { recruitUnit('skeletonMilitia'); }; // Use anonymous func or direct ref recruitSlingerButton.onClick = function () { recruitUnit('skeletonSlinger'); }; updateGoldDisplay(); // Set initial gold text and button states based on starting gold (0) // --- 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); } } // 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.
===================================================================
--- original.js
+++ change.js
@@ -66,9 +66,9 @@
// 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
+ // --- Define Methods First ---
self.findTarget = function (enemyArray) {
var closestTarget = null;
var minDistance = Infinity;
// Make sure enemyArray is defined and is an array
@@ -76,9 +76,10 @@
return null;
}
for (var i = 0; i < enemyArray.length; i++) {
var enemy = enemyArray[i];
- if (enemy.isDead) {
+ if (!enemy || enemy.isDead) {
+ // Added check for enemy existence
continue;
}
// Using horizontal distance for simplicity
var distance = Math.abs(self.x - enemy.x);
@@ -118,14 +119,14 @@
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
+ // LK.effects.flashObject(self.graphics, 0xFF0000, 100); // Flash the graphics asset
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
+ // Award gold based on unit type/strength (consider using self.unitType later)
+ gold += 5; // Base gold reward (TODO: Make this dynamic based on enemy type)
updateGoldDisplay(); // Update the display immediately
}
self.die();
}
@@ -138,11 +139,73 @@
} catch (e) {
console.log("Sound error playing unitDeath: " + e);
}
self.isDead = true;
+ self.interactive = false; // Disable interaction when dead
// It will be removed from the game and array in game.update
}
};
+ self.upgrade = function () {
+ if (self.isEnemy || !self.upgradeTo || !isBattling || self.isDead) {
+ console.log("Upgrade condition not met.");
+ return; // Can only upgrade living player units during battle with a valid upgrade path
+ }
+ var upgradeStats = UNIT_STATS[self.upgradeTo];
+ if (!upgradeStats) {
+ console.error("Cannot find stats for upgrade target:", self.upgradeTo);
+ return;
+ }
+ var cost = self.upgradeCost;
+ if (gold >= cost) {
+ gold -= cost;
+ updateGoldDisplay();
+ // Play sound (optional)
+ try {
+ LK.getSound('upgradeUnit').play();
+ } catch (e) {
+ console.log("Sound error playing upgradeUnit: " + e);
+ }
+ console.log("Upgrading " + self.unitType + " to " + self.upgradeTo + " for " + cost + "g");
+ // --- Update Stats ---
+ var oldMaxHp = self.maxHp;
+ var currentHpRatio = self.hp / oldMaxHp; // Preserve HP percentage
+ self.unitType = self.upgradeTo; // Update the type identifier
+ self.attackPower = upgradeStats.attackPower;
+ self.attackRange = upgradeStats.attackRange;
+ self.speed = upgradeStats.speed;
+ self.maxHp = upgradeStats.hp;
+ self.hp = currentHpRatio * self.maxHp; // Scale current HP
+ self.attackCooldownTime = upgradeStats.attackCooldownTime || 60; // Update cooldown time
+ self.attackSoundId = upgradeStats.attackSoundId || 'attackMelee'; // Update sound
+ // Update future upgrade path
+ self.upgradeTo = upgradeStats.upgradeTo;
+ self.upgradeCost = upgradeStats.upgradeCost;
+ // --- Update Graphics (if asset changes) ---
+ if (self.assetId !== upgradeStats.assetId) {
+ self.assetId = upgradeStats.assetId;
+ if (self.graphics) {
+ self.removeChild(self.graphics); // Remove old graphics
+ }
+ // Attach new graphics
+ self.graphics = self.attachAsset(self.assetId, {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ // Add visual feedback for upgrade?
+ LK.effects.flashObject(self.graphics, 0xFFFF00, 300); // Yellow flash
+ } else {
+ // Flash existing graphics if asset doesn't change
+ LK.effects.flashObject(self.graphics, 0xFFFF00, 300); // Yellow flash
+ }
+ // If no further upgrade, disable interaction for upgrading?
+ // Or just let the check `!self.upgradeTo` handle it.
+ console.log("Upgrade complete. New type:", self.unitType);
+ } else {
+ console.log("Not enough gold to upgrade. Cost:", cost, "Have:", gold);
+ // Optional: Flash the unit or gold display red
+ LK.effects.flashObject(self.graphics, 0xFF0000, 200);
+ }
+ };
// 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) {
@@ -166,29 +229,56 @@
self.move();
}
self.lastX = self.x; // Update last known state
};
+ // --- Event Handlers ---
+ // Called automatically by LK engine if interactive
+ self.down = function (x, y, obj) {
+ // Only allow upgrading player units during battle
+ if (!self.isEnemy && isBattling && !self.isDead) {
+ console.log("Player unit tapped:", self.unitType, " Gold:", gold, " Upgrade Cost:", self.upgradeCost);
+ // Attempt upgrade immediately on tap
+ if (self.upgradeTo && self.upgradeCost > 0) {
+ self.upgrade();
+ } else {
+ console.log("No upgrade available for this unit.");
+ // Optional feedback: maybe a small visual effect?
+ }
+ }
+ };
// --- Initialize ---
+ var stats = UNIT_STATS[config.unitType]; // Get stats based on type
+ if (!stats) {
+ console.error("Unit creation failed: Invalid unitType", config.unitType);
+ return null; // Prevent creation if stats are missing
+ }
+ self.unitType = config.unitType; // Store the specific type string
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.hp = stats.hp;
+ self.maxHp = stats.hp; // Store max HP
+ self.attackPower = stats.attackPower;
+ self.attackRange = stats.attackRange;
+ self.attackCooldownTime = stats.attackCooldownTime || 60; // Default 1 sec cooldown
+ self.speed = stats.speed;
+ self.assetId = stats.assetId;
+ self.attackSoundId = stats.attackSoundId || 'attackMelee';
+ // Upgrade properties (may be null/0 if no upgrade available)
+ self.upgradeTo = stats.upgradeTo || null;
+ self.upgradeCost = stats.upgradeCost || 0;
// Attach graphics *after* properties are set
self.graphics = self.attachAsset(self.assetId, {
anchorX: 0.5,
- anchorY: 0.5
+ // Centered anchor
+ anchorY: 0.5 // Centered anchor
});
// 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
+ // Make units interactive so they can be tapped
+ self.interactive = true;
return self;
});
/****
@@ -216,28 +306,72 @@
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
// Unit stats lookup table
+// Unit stats lookup table, including upgrade info
var UNIT_STATS = {
+ // --- Player Skeletons ---
skeletonMilitia: {
hp: 50,
attackPower: 5,
attackRange: 80,
+ // Melee range
speed: 1.5,
assetId: 'skeletonMilitia',
cost: 10,
- attackSoundId: 'attackMelee'
+ // Recruitment cost
+ attackSoundId: 'attackMelee',
+ upgradeTo: 'skeletonRookie',
+ // Next unit type
+ upgradeCost: 20 // Gold cost to upgrade
},
+ skeletonRookie: {
+ // Placeholder stats for upgrade target
+ hp: 75,
+ attackPower: 8,
+ attackRange: 85,
+ speed: 1.6,
+ assetId: 'skeletonMilitia',
+ // Placeholder: Use same asset for now
+ cost: 0,
+ // Not directly recruitable
+ attackSoundId: 'attackMelee',
+ upgradeTo: null,
+ // No further upgrade defined yet
+ upgradeCost: 0
+ },
skeletonSlinger: {
hp: 30,
attackPower: 7,
attackRange: 400,
+ // Ranged
speed: 1.2,
assetId: 'skeletonSlinger',
cost: 15,
+ // Recruitment cost
attackCooldownTime: 90,
- attackSoundId: 'attackRanged'
+ attackSoundId: 'attackRanged',
+ upgradeTo: 'skeletonTraineeArcher',
+ // Next unit type
+ upgradeCost: 25 // Gold cost to upgrade
},
+ skeletonTraineeArcher: {
+ // Placeholder stats for upgrade target
+ hp: 45,
+ attackPower: 10,
+ attackRange: 450,
+ speed: 1.3,
+ assetId: 'skeletonSlinger',
+ // Placeholder: Use same asset for now
+ cost: 0,
+ // Not directly recruitable
+ attackCooldownTime: 85,
+ attackSoundId: 'attackRanged',
+ upgradeTo: null,
+ // No further upgrade defined yet
+ upgradeCost: 0
+ },
+ // --- Human Enemies ---
humanFarmer: {
hp: 40,
attackPower: 4,
attackRange: 70,
@@ -253,9 +387,11 @@
assetId: 'humanSlinger',
attackCooldownTime: 90,
attackSoundId: 'attackRanged'
}
+ // Add other unit stats here as needed...
};
+// Add sounds for upgrade action (optional)
// Game state variables - Defined globally for access within functions and update loop
var gold = 10;
var playerUnits = [];
var enemyUnits = [];
@@ -373,17 +509,21 @@
console.error("Cannot spawn unknown unit type:", unitType);
return null;
}
// Prepare config for the Unit class constructor
+ // 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 specific type string
+ isEnemy: isEnemy
+ // Stats are now looked up inside the Unit constructor based on unitType
+ // hp: stats.hp, // No longer needed here
+ // attackPower: stats.attackPower, // No longer needed here
+ // attackRange: stats.attackRange, // No longer needed here
+ // speed: stats.speed, // No longer needed here
+ // assetId: stats.assetId, // No longer needed here
+ // attackCooldownTime: stats.attackCooldownTime, // No longer needed here
+ // attackSoundId: stats.attackSoundId // No longer needed here
};
// Create new instance of Unit
var unit = new Unit(config);
// Determine spawn side and array
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