User prompt
Add new type of unit: Defender! Defender type units are melee type units that have high health but have low damage; humans don't have them yet, but your army does! (Unlocking this class requires research; Units [from weakest to stronghest]: Shielded skeleton, Skeleton holpite)]
User prompt
Add new enemies: Melee: human elite guard; Ranged: human archer
User prompt
Units never get faster
User prompt
Units never get faster
User prompt
Skeleton swordsman uses the asset skeletonSwordsman and Skeleton archer uses asset skeletonArcher
User prompt
Add new units: melee: Skeleton swordsman; Ranged: Skeleton archer. Note that these units are stronger than rookie and trainee archer, so they can be researched AFTER researching Rookie or trainee
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.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) {
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';
// 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
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x101020 // Dark background for the war theme
});
/****
* Game Code
****/
// Game constants and configuration
// LK Engine automatically creates assets based on usage.
// Define logical assets for units and UI.
// White box
// Gray ellipse
// Green box
// Blue ellipse
// Button background
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
var UNIT_STATS = {
skeletonMilitia: {
hp: 50,
attackPower: 5,
attackRange: 80,
speed: 1.5,
assetId: 'skeletonMilitia',
cost: 10,
attackSoundId: 'attackMelee'
},
skeletonSlinger: {
hp: 30,
attackPower: 7,
attackRange: 400,
speed: 1.2,
assetId: 'skeletonSlinger',
cost: 15,
attackCooldownTime: 90,
attackSoundId: 'attackRanged'
},
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'
}
};
// Game state variables - Defined globally for access within functions and update loop
var gold = 0;
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
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
};
// 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)
currentEnemyConfig = {
humanFarmer: 3,
humanSlinger: 1
};
// 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();
// 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
@@ -70,8 +70,12 @@
// 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;
@@ -190,16 +194,16 @@
/****
* 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
+// LK Engine automatically creates assets based on usage.
+// Define logical assets for units and UI.
+// White box
+// Gray ellipse
+// Green box
+// Blue ellipse
+// Button background
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var PLAYER_SPAWN_X = 150;
var ENEMY_SPAWN_X = GAME_WIDTH - 150;
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