User prompt
Add new unit: Alchemist (a fragile unit that brews one of these 3 potions: Regular potion [this potion deals area damage to enemies and makes an acid pool that deals damage over time to the enemies that step on it], Heal potion [heals allies in an area and makes a heal pool that makes them heal], Acidic mixture dip [buffs an ally with a special syrum that makes them deal more damage, attack faster and deal more damage to non-living things for 5 attacks]. Faction: magic)
User prompt
Add new unit: Alchemist (a fragile unit that brews one of these 3 potions: Regular potion [this potion deals area damage to enemies and makes an acid pool that deals damage over time to the enemies that step on it], Heal potion [heals allies in an area and makes a heal pool that makes them heal], Acidic mixture dip [buffs an ally with a special syrum that makes them deal more damage, attack faster and deal more damage to non-living things for 5 attacks]. Faction: magic)
User prompt
Now you can change maps! (Maps: plains, farm, village, castle, pirate ship, colloseum, arena of ice, magical forest, modern battlefield, Dinosaur jungle, Science lab)
User prompt
Now you can change maps! (Maps: plains, farm, village, castle, pirate ship, colloseum, arena of ice, magical forest, modern battlefield, Dinosaur jungle, Science lab)
User prompt
Add new unit: Viking bear rider! Beefy, area damage unit that, when killed, makes a hammer viking
User prompt
Remove Valkyrie
User prompt
Valkyrie is now immune to ground melee attacks and cannot be targeted by them
User prompt
Valkyrie still gets hit by ground melee units
User prompt
Make it so that Valkyrie is also flying (meaning that it is immune to melee) and is the only melee unit that can hit planes
User prompt
Plane uses destroyed sound
User prompt
Pineapples now do 150 damage
User prompt
Plane is technically in the air, so make it that it is immune to exploding pineapples and make it fly away when it drops one
User prompt
Exploding pineapples don't fall over, instead, they stay still and explode after 3 seconds, and plane should still fly at enemies to drop those pineapples at them and then fly away to the next unit (and also make it so that plane can only be hit by ranged units)
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'parent')' in or related to this line: 'if (!self.projectiles[i].parent) {' Line Number: 148
User prompt
Plane isn't in modern era faction; fix that
User prompt
Plane isn't in modern era faction
User prompt
Add new unit: Plane! Flies in random areas all over the battlefield, launching exploding pineapples all over it! (Faction: modern era)
User prompt
Add new unit: Plane! Flies in random areas all over the battlefield, launching exploding pineapples all over the it! (Faction: modern era)
User prompt
When a chicken dies, use the "bawk" sound
User prompt
Please fix the bug: 'TypeError: Cannot read properties of null (reading 'x')' in or related to this line: 'var dist = getDistance(self.x, self.y, self.target.x, self.target.y);' Line Number: 1138
User prompt
Add new units: Ballista: A tower taht shoots big arrows in a nearly infinite range! (Faction: Rome); Chicken: A chicken that has a bomb strapped in it's chest, exploding when it sees any enemy! (Faction: farm); Apprentice: a weaker wizard (faction: magic)
User prompt
Scarecrow is not a living thing
User prompt
Make apple thrower throw apples at allies
User prompt
Add new unit: Helicopter! This helicopter fliest over the units, shooting bullets at them at a good speed; also has Razor rotors in case enemies are near it! (Faction: modern era)
User prompt
Add new unit: Helicopter! This helicopter fliest over the units, shooting bullets at them at a good speed; also has Razor rotors in case enemies are near it! (Faction: modern era)
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Battlefield = Container.expand(function () { var self = Container.call(this); self.width = 2048; self.height = 1366; self.initialized = false; self.pools = []; // Array to hold active pools // Create background var bg = self.attachAsset('background', { anchorX: 0, anchorY: 0 }); self.projectiles = []; self.createProjectile = function (source, target, type) { var projectile = new Projectile(source, target, type); projectile.x = source.x; projectile.y = source.y; self.addChild(projectile); self.projectiles.push(projectile); }; self.createPool = function (type, x, y, sourceTeam) { var pool = new Pool(type, x, y, sourceTeam); self.addChild(pool); self.pools.push(pool); }; self.removeUnit = function (unit) { var index = units.indexOf(unit); if (index !== -1) { units.splice(index, 1); } // Check win condition self.checkWinCondition(); }; self.checkWinCondition = function () { var redTeamUnits = 0; var blueTeamUnits = 0; for (var i = 0; i < units.length; i++) { if (units[i].active) { if (units[i].team === 'red') { redTeamUnits++; } else { blueTeamUnits++; } } } if (self.initialized && (redTeamUnits === 0 || blueTeamUnits === 0)) { var winningTeam = redTeamUnits > 0 ? "Red" : "Blue"; if (winText) { winText.setText(winningTeam + " Team Wins!"); winText.visible = true; } LK.getSound('victory').play(); // Create reset button if (!resetButton) { resetButton = new ResetButton(); resetButton.x = self.width / 2; resetButton.y = self.height / 2 + 100; game.addChild(resetButton); } } }; self.update = function () { // Update all projectiles for (var i = self.projectiles.length - 1; i >= 0; i--) { var projectile = self.projectiles[i]; // Check if projectile still exists before updating // It might have been destroyed by another projectile's update in the same frame if (!projectile || !projectile.parent) { // If it was already destroyed or doesn't exist, remove it from the array. self.projectiles.splice(i, 1); continue; // Skip to the next iteration } // Update the projectile projectile.update(); // Re-check if the projectile was destroyed *during* its own update call // If so, remove it from the array. This is the primary cleanup mechanism. if (!projectile.parent) { self.projectiles.splice(i, 1); } } // Update all pools for (var i = self.pools.length - 1; i >= 0; i--) { var pool = self.pools[i]; if (!pool || !pool.parent) { // Already destroyed or removed, splice it out self.pools.splice(i, 1); continue; } pool.update(); // Re-check after update in case it destroyed itself if (!pool.parent) { self.pools.splice(i, 1); } } }; return self; }); var Bomb = Container.expand(function (source, target) { var self = Container.call(this); self.source = source; self.damage = source.attackDamage; self.team = source.team; self.explosionRadius = 150; self.timer = 120; // 2 seconds at 60 FPS // Create bomb appearance var bombGraphics = self.attachAsset('bomb', { anchorX: 0.5, anchorY: 0.5 }); // Add countdown text self.countdownText = new Text2("2.0", { size: 20, fill: 0xFFFFFF }); self.countdownText.anchor.set(0.5, 0.5); self.countdownText.y = -20; self.addChild(self.countdownText); // Throw bomb in direction of target if (target) { var dx = target.x - source.x; var dy = target.y - source.y; var dist = Math.sqrt(dx * dx + dy * dy); // Normalize direction var dirX = dx / dist; var dirY = dy / dist; // Set initial position self.x = source.x; self.y = source.y; // Calculate throw destination (somewhere near the target) var throwDistance = Math.min(dist, source.attackRange); self.destX = source.x + dirX * throwDistance; self.destY = source.y + dirY * throwDistance; // Add some randomness to the landing position self.destX += (Math.random() - 0.5) * 50; self.destY += (Math.random() - 0.5) * 50; // Initial velocity for arc trajectory self.velY = -10; // Initial upward velocity } self.update = function () { // Update timer self.timer--; // Update countdown text self.countdownText.setText((self.timer / 60).toFixed(1)); // Animate bomb flying through the air in an arc if (self.destX !== undefined) { // Calculate progress (0 to 1) var progress = 1 - self.timer / 120; if (progress < 0.5) { // First half of flight - move in arc self.x = self.source.x + (self.destX - self.source.x) * progress * 2; // Calculate y with arc var baseY = self.source.y + (self.destY - self.source.y) * progress * 2; var arcHeight = -100 * Math.sin(progress * 2 * Math.PI); self.y = baseY + arcHeight; // Rotate bomb as it flies self.rotation = progress * Math.PI * 4; } else { // Second half - bomb has landed self.x = self.destX; self.y = self.destY; self.rotation = 0; } } // Flash the bomb when it's about to explode if (self.timer < 30) { bombGraphics.alpha = Math.abs(Math.sin(self.timer * 0.4)) * 0.5 + 0.5; } // Explode when timer reaches zero if (self.timer <= 0) { self.explode(); } }; self.explode = function () { // Play explosion sound LK.getSound('Explosion').play(); // Create explosion visual effect var explosion = new Container(); var explosionGraphics = explosion.attachAsset('bomb', { anchorX: 0.5, anchorY: 0.5, width: self.explosionRadius * 2, height: self.explosionRadius * 2, color: 0xFF5500 }); explosion.x = self.x; explosion.y = self.y; explosion.alpha = 0.8; battlefield.addChild(explosion); // Fade out explosion tween(explosion, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 500, onFinish: function onFinish() { explosion.destroy(); } }); // Deal damage to nearby units for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active) { var dist = getDistance(self.x, self.y, unit.x, unit.y); if (dist <= self.explosionRadius) { // Damage falls off with distance var damageMultiplier = 1 - dist / self.explosionRadius; var damage = Math.floor(self.damage * damageMultiplier); // Deal damage to all units in range (even friendly units, but with reduced damage) if (unit.team !== self.team) { unit.takeDamage(damage, self.source); } else if (unit !== self.source) { // Deal reduced friendly fire damage unit.takeDamage(Math.floor(damage * 0.3), self.source); } } } } // Remove the bomb self.destroy(); }; return self; }); var EndlessModeButton = Container.expand(function () { var self = Container.call(this); var buttonShape = self.attachAsset('button', { anchorX: 0.5, anchorY: 0.5 }); var buttonText = new Text2("Endless Mode", { size: 40, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.down = function (x, y, obj) { endlessMode.start(); self.visible = false; instructions.visible = false; LK.playMusic('battleMusic'); }; return self; }); var EndlessModeInstructionsButton = Container.expand(function () { var self = Container.call(this); var buttonShape = self.attachAsset('button', { anchorX: 0.5, anchorY: 0.5 }); var buttonText = new Text2("Instructions for Endless Mode", { size: 30, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.down = function (x, y, obj) { endlessModeInstructions.visible = !endlessModeInstructions.visible; if (endlessModeInstructions.visible) { // Create a black opaque overlay var overlay = new Container(); var overlayShape = overlay.attachAsset('background', { anchorX: 0.5, anchorY: 0.5, width: 2048, height: 2732, color: 0x000000, alpha: 0.7 }); overlay.x = 2048 / 2; overlay.y = 2732 / 2; game.addChild(overlay); // Create a custom popup for Endless Mode Instructions var popupText = new Text2("Endless Mode Instructions:\n- Place units on the battlefield.\n- Survive waves of enemies.\n- Unlock new units as you progress.\n- Win by unlocking all units.", { size: 30, fill: 0xFFFFFF }); popupText.anchor.set(0.5, 0.5); popupText.x = 2048 / 2; popupText.y = 2732 / 2; game.addChild(popupText); // Remove popup and overlay when tapping anywhere overlay.down = function (x, y, obj) { if (popupText.parent) { popupText.destroy(); } if (overlay.parent) { overlay.destroy(); } endlessModeInstructions.visible = false; // Hide the instructions text }; } if (endlessModeInstructions.visible) { // Create a custom popup for Endless Mode Instructions var popupText = new Text2("Endless Mode Instructions:\n- Place units on the battlefield.\n- Survive waves of enemies.\n- Unlock new units as you progress.\n- Win by unlocking all units.", { size: 30, fill: 0xFFFFFF }); popupText.anchor.set(0.5, 0.5); popupText.x = 2048 / 2; popupText.y = 2732 / 2; game.addChild(popupText); } }; return self; }); var FactionButton = Container.expand(function (team, faction, x, y) { var self = Container.call(this); self.team = team; self.faction = faction; self.x = x; self.y = y; // Create button appearance var buttonShape = self.attachAsset('button' + team.charAt(0).toUpperCase() + team.slice(1), { anchorX: 0.5, anchorY: 0.5, width: 150, height: 80 }); // Add faction name text var nameText = new Text2(faction.charAt(0).toUpperCase() + faction.slice(1), { size: 24, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0.5); nameText.y = 30; self.addChild(nameText); self.down = function (x, y, obj) { // When button is pressed, create unit buttons for the faction var xOffset = 120; for (var i = 0; i < factions[faction].length; i++) { var unitButton = new UnitButton(self.team, factions[faction][i], 2048 - xOffset, self.y); game.addChild(unitButton); xOffset += 180; } // Highlight button LK.effects.flashObject(self, 0xffffff, 300); }; return self; }); var Pineapple = Container.expand(function (source) { var self = Container.call(this); //{Pineapple_1} self.source = source; //{Pineapple_2} self.damage = 150; //{Pineapple_3} // Pineapple explosion damage set to 150 self.team = source.team; // Inherit team from plane self.explosionRadius = 100; // Radius of pineapple explosion // self.fallSpeed = 5; // How fast the pineapple falls - Removed, pineapple stays still self.explosionTimer = 180; // 3 seconds * 60 FPS // Create pineapple appearance var pineappleGraphics = self.attachAsset('pineapple', { anchorX: 0.5, //{Pineapple_4} anchorY: 0.5 //{Pineapple_5} }); //{Pineapple_6} self.x = source.x; self.y = source.y; self.update = function () { //{Pineapple_7} // Pineapple stays still // self.y += self.fallSpeed; // Removed // self.rotation += 0.1; // Optional: Keep spinning? Let's remove for now. // Update timer self.explosionTimer--; // Explode when timer reaches zero if (self.explosionTimer <= 0) { self.explode(); return; // Stop updating once exploded } //{Pineapple_8} // Removed ground check and out of bounds check as it doesn't move vertically }; //{Pineapple_10} self.explode = function () { // Play explosion sound LK.getSound('Explosion').play(); //{Pineapple_11} // Create explosion visual effect//{Pineapple_12} var explosion = new Container(); //{Pineapple_13} var explosionGraphics = explosion.attachAsset('bomb', { //{Pineapple_14} // Reuse bomb graphic for explosion anchorX: 0.5, //{Pineapple_15} anchorY: 0.5, //{Pineapple_16} width: self.explosionRadius * 2, height: self.explosionRadius * 2, color: 0xFFFF00 // Yellow explosion for pineapple }); //{Pineapple_17} explosion.x = self.x; //{Pineapple_18} explosion.y = self.y; //{Pineapple_19} // Explode where it landed explosion.alpha = 0.8; //{Pineapple_20} battlefield.addChild(explosion); //{Pineapple_21} // Fade out explosion//{Pineapple_22} tween(explosion, { //{Pineapple_23} alpha: 0, //{Pineapple_24} scaleX: 1.3, //{Pineapple_25} scaleY: 1.3 //{Pineapple_26} }, { //{Pineapple_27} duration: 400, //{Pineapple_28} onFinish: function onFinish() { //{Pineapple_29} explosion.destroy(); //{Pineapple_30} } //{Pineapple_31} }); //{Pineapple_32} // Deal damage to nearby units for (var i = 0; i < units.length; i++) { //{Pineapple_33} var unit = units[i]; //{Pineapple_34} // Check if the unit is active AND not a plane before dealing damage if (unit.active && unit.type !== 'plane') { var dist = getDistance(self.x, self.y, unit.x, unit.y); //{Pineapple_35} if (dist <= self.explosionRadius) { // Damage falls off with distance//{Pineapple_36} var damageMultiplier = 1 - dist / self.explosionRadius; var damage = Math.floor(self.damage * damageMultiplier); // Deal damage (potentially friendly fire, but not to planes) unit.takeDamage(damage, self.source); // Attribute damage to the plane } //{Pineapple_37} } //{Pineapple_38} // Skip inactive units and planes } //{Pineapple_39} // Remove the pineapple // Let Battlefield.update handle removal from the projectiles array self.destroy(); //{Pineapple_41} }; //{Pineapple_42} return self; //{Pineapple_43} }); var Pool = Container.expand(function (type, x, y, sourceTeam) { var self = Container.call(this); self.type = type; // 'acid' or 'healing' self.x = x; self.y = y; self.sourceTeam = sourceTeam; self.duration = 300; // 5 seconds self.tickInterval = 30; // Apply effect every 0.5 seconds self.tickTimer = self.tickInterval; self.radius = 60; // Half the width/height of the asset var poolAssetId = type === 'acid' ? 'acidPool' : 'healingPool'; var poolGraphics = self.attachAsset(poolAssetId, { anchorX: 0.5, anchorY: 0.5, alpha: 0.6 }); self.update = function () { self.duration--; self.tickTimer--; if (self.tickTimer <= 0) { self.applyEffect(); self.tickTimer = self.tickInterval; } if (self.duration <= 0) { // Fade out and destroy tween(self, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { if (self.parent) { // Find in battlefield pools and remove var index = battlefield.pools.indexOf(self); if (index !== -1) { battlefield.pools.splice(index, 1); } self.destroy(); } } }); } }; self.applyEffect = function () { for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active) { var dist = getDistance(self.x, self.y, unit.x, unit.y); if (dist <= self.radius) { if (self.type === 'acid' && unit.team !== self.sourceTeam) { // Acid damages enemies unit.takeDamage(5, self); // Small DoT damage } else if (self.type === 'healing' && unit.team === self.sourceTeam) { // Healing heals allies if (unit.health < unit.maxHealth) { unit.takeDamage(-8, self); // Heal amount (negative damage) LK.effects.flashObject(unit, 0x00ff00, 150); // Quick green flash } } } } } }; return self; }); var Projectile = Container.expand(function (source, target, type) { var self = Container.call(this); self.source = source; self.target = target; self.speed = type === 'arrow' ? 12 : 6; self.damage = source.attackDamage; var projectileAsset = self.attachAsset(type, { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { if (!self.target || !self.target.active) { self.destroy(); return; } // Calculate direction to target if (!self.target) { self.destroy(); return; } if (self.target) { if (self.target) { var dx = self.target.x - self.x; var dy = self.target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); } else { self.destroy(); return; } } else { self.destroy(); return; } // Rotate projectile to face target self.rotation = Math.atan2(dy, dx); // Move towards target if (dist > self.speed) { self.x += dx / dist * self.speed; self.y += dy / dist * self.speed; } else { // Hit target // Handle Alchemist potions first if (self.source && self.source.type === 'alchemist') { if (type === 'regularPotion' && self.target) { // Regular Potion: AoE Damage + Acid Pool var explosionRadius = 80; var baseDamage = self.damage; // Use alchemist's damage stat LK.getSound('Explosion').play(); // Use explosion sound? Or a splash? // Deal AoE damage for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.source.team) { var dist = getDistance(self.target.x, self.target.y, unit.x, unit.y); if (dist <= explosionRadius) { var damageMultiplier = 1 - dist / explosionRadius; var damage = Math.floor(baseDamage * damageMultiplier); if (damage > 0) { unit.takeDamage(damage, self.source); } } } } // Create Acid Pool battlefield.createPool('acid', self.target.x, self.target.y, self.source.team); } else if (type === 'healPotion' && self.target) { // Heal Potion: AoE Heal + Healing Pool var healRadius = 90; var baseHeal = Math.abs(self.damage); // Use alchemist's damage as heal amount LK.getSound('Holy').play(); // Use holy sound? // Heal nearby allies for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team === self.source.team) { var dist = getDistance(self.target.x, self.target.y, unit.x, unit.y); if (dist <= healRadius) { var healMultiplier = 1 - dist / healRadius; var healAmount = Math.floor(baseHeal * healMultiplier); if (healAmount > 0 && unit.health < unit.maxHealth) { unit.takeDamage(-healAmount, self.source); // Negative damage to heal LK.effects.flashObject(unit, 0x00ff00, 300); } } } } // Create Healing Pool battlefield.createPool('healing', self.target.x, self.target.y, self.source.team); } else if (type === 'acidicMixturePotion' && self.target && self.target.team === self.source.team) { // Acidic Mixture Dip: Buff Ally self.target.applyBuff('acidicDip', self.source); // Apply buff directly LK.getSound('spell').play(); // Use spell sound? LK.effects.flashObject(self.target, 0xffff00, 500); // Yellow flash } // Destroy potion projectile after effect self.destroy(); return; // Potion handling complete } else if (type === 'arrow') { if (self.target.type === 'mirrorShield' && Math.random() < 0.5) { // 50% chance to reflect // Reflect the projectile back to the source self.target = self.source; self.source = null; // No longer has a source LK.getSound('Defend').play(); // Play 'Defend' sound } else { self.target.takeDamage(self.damage, self.source); } } else if (type === 'rock') { // Deal damage to the target self.target.takeDamage(self.damage, self.source); // Split into smaller rocks for (var i = 0; i < 3; i++) { var smallRock = new Projectile(self, self.target, 'smallRock'); smallRock.x = self.x; smallRock.y = self.y; smallRock.speed = 4; // Smaller rocks move slower battlefield.addChild(smallRock); battlefield.projectiles.push(smallRock); } } else if (type === 'javelin') { var piercedEnemies = 0; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.source.team) { var distToUnit = getDistance(self.x, self.y, unit.x, unit.y); if (distToUnit < 50) { // Assuming a small radius for piercing unit.takeDamage(self.damage, self.source); piercedEnemies++; if (piercedEnemies >= 3) { break; } // Stop after piercing 3 enemies } } } } self.destroy(); } }; return self; }); var ResetButton = Container.expand(function () { var self = Container.call(this); var buttonShape = self.attachAsset('button', { anchorX: 0.5, anchorY: 0.5 }); var buttonText = new Text2("Reset Battle", { size: 40, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.down = function (x, y, obj) { // Reset the battle resetBattle(); }; return self; }); var StartButton = Container.expand(function () { var self = Container.call(this); var buttonShape = self.attachAsset('button', { anchorX: 0.5, anchorY: 0.5 }); var buttonText = new Text2("Start Battle!", { size: 40, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); self.addChild(buttonText); self.down = function (x, y, obj) { // Start the battle (mark battlefield as initialized) if (units.length > 0) { battlefield.initialized = true; self.visible = false; instructions.visible = false; LK.playMusic('battleMusic'); } }; return self; }); var Unit = Container.expand(function (team, type, x, y) { var self = Container.call(this); self.team = team; self.type = type; self.x = x; self.y = y; self.active = true; self.target = null; self.attackCooldown = 0; self.attackRange = 0; self.attackDamage = 0; self.moveSpeed = 0; self.health = 100; self.maxHealth = 100; self.special = ""; self.isLiving = true; // New property to categorize living vs non-living units // Buff properties self.buffType = null; self.buffAttackCount = 0; self.buffSource = null; // To attribute buff effects if needed self.buffDamageMultiplier = 1.0; // Multiplier for damage dealt self.buffAttackSpeedMultiplier = 1.0; // Multiplier for attack speed (lower cooldown) self.buffNonLivingBonus = 0; // Flat bonus damage vs non-living // Default properties based on unit type switch (type) { case 'ballista': self.attackRange = 800; // Nearly infinite range self.attackDamage = 40; self.moveSpeed = 0; // Cannot move self.health = 150; self.maxHealth = 150; self.attackCooldown = 120; // 2 seconds at 60 FPS self.special = "big arrows with long range"; self.isLiving = false; // Not a living entity self.attackType = 'ranged'; // Identify attack type self.flying = false; break; case 'vikingBearRider': self.attackRange = 90; // Melee range self.attackDamage = 35; // Decent melee damage self.moveSpeed = 0.9; // Slightly slower self.health = 250; // Beefy self.maxHealth = 250; self.attackCooldown = 80; // Slower attack speed self.special = "area damage, spawns hammer viking on death"; self.isLiving = true; // Living unit (bear) self.attackType = 'melee'; // Identify attack type self.flying = false; break; case 'chicken': self.attackRange = 150; // Needs to get close to explode self.attackDamage = 200; // High explosion damage self.moveSpeed = 1.8; // Fast movement self.health = 20; self.maxHealth = 20; self.attackCooldown = 60; // Only explodes once self.special = "explodes on enemy contact"; self.isLiving = true; break; case 'apprentice': self.attackRange = 200; // Less range than wizard self.attackDamage = 5; // Less damage than wizard self.moveSpeed = 1.1; // Faster than wizard self.health = 50; self.maxHealth = 50; self.attackCooldown = 80; self.special = "weak magic"; break; case 'jeep': self.attackRange = 500; self.attackDamage = 7; self.moveSpeed = 1.2; self.health = 250; self.maxHealth = 250; self.attackCooldown = 20; // Fire every 0.333 seconds (20 frames at 60 FPS) self.special = "vehicle, machine gun"; self.isLiving = false; // Vehicles are not living break; case 'alchemist': self.attackRange = 300; self.attackDamage = 15; // Base damage for regular potion, base heal for heal potion self.moveSpeed = 0.9; self.health = 65; // Fragile self.maxHealth = 65; self.attackCooldown = 150; // Slower attack rate self.special = "brews random potions (damage, heal, buff)"; self.isLiving = true; self.attackType = 'ranged'; self.flying = false; break; case 'gatlingGunner': self.attackRange = Infinity; // Infinite range self.attackDamage = 8; // Increased damage per bullet self.moveSpeed = 0; // Stands still self.health = 80; self.maxHealth = 80; self.attackCooldown = 10; // Very fast shooting self.special = "fast but inaccurate shooting"; self.isLiving = false; // Gatling guns are not living break; case 'mortar': self.attackRange = Infinity; self.attackDamage = 30; self.moveSpeed = 0; // Mortar cannot move self.health = 50; // Fragile self.maxHealth = 50; self.attackCooldown = 150; self.special = "high area damage, infinite range"; break; case 'wobbler': self.attackRange = 50; self.attackDamage = 15; self.moveSpeed = 1.5; self.attackCooldowsn = 40; break; case 'farmer': self.attackRange = 70; self.attackDamage = 25; // High first hit self.moveSpeed = 1.2; self.attackCooldown = 60; self.special = "extra damage on first hit"; break; case 'skeleton': self.attackRange = 70; self.attackDamage = 25; self.moveSpeed = 1.2; self.attackCooldown = 60; self.special = "extra damage on first hit"; break; case 'archer': self.attackRange = 400; self.attackDamage = 12; self.moveSpeed = 1.0; self.attackCooldown = 80; self.special = "ranged"; break; case 'shield': self.attackRange = 60; self.attackDamage = 8; self.moveSpeed = 0.8; self.health = 150; self.maxHealth = 150; self.attackCooldown = 50; self.special = "resistant to arrow"; break; case 'sword': self.attackRange = 80; self.attackDamage = 20; self.moveSpeed = 1.3; self.attackCooldown = 45; break; case 'romanLegionnaire': self.attackRange = 80; self.attackDamage = 20; self.moveSpeed = 1.0; self.health = 150; self.maxHealth = 150; self.attackCooldown = 50; self.special = "invincibility for 1 second every 5 seconds"; // Add special ability description break; case 'wizard': self.attackRange = 300; self.attackDamage = 10; self.moveSpeed = 0.9; self.attackCooldown = 100; self.special = "splash damage"; break; case 'witch': self.attackRange = 400; self.attackDamage = 25; self.moveSpeed = 0.8; self.attackCooldown = 120; self.special = "magic ball"; break; case 'necromancer': self.attackRange = 100; self.attackDamage = 5; self.moveSpeed = 0.7; self.health = 80; self.maxHealth = 80; self.attackCooldown = 90; self.special = "summons skeletons"; break; case 'berserker': self.attackRange = 70; self.attackDamage = 20; self.moveSpeed = 1.5; self.attackCooldown = 50; self.special = "double damage at half health"; break; case 'spearman': self.attackRange = 100; self.attackDamage = 18; self.moveSpeed = 1.2; self.attackCooldown = 60; self.special = "chance to throw spear"; break; case 'mirrorShield': self.attackRange = 60; self.attackDamage = 5; self.moveSpeed = 0.8; self.health = 120; self.maxHealth = 120; self.attackCooldown = 50; self.special = "chance to reflect projectiles"; break; case 'king': self.attackRange = 80; self.attackDamage = 50; self.moveSpeed = 0.5; self.health = 300; self.maxHealth = 300; self.attackCooldown = 100; self.special = "massive damage and health"; break; case 'crossbowMan': self.attackRange = 400; self.attackDamage = 15; self.moveSpeed = 1.0; self.attackCooldown = 60; self.special = "fast firing, chance for critical damage"; break; case 'sniper': self.attackRange = Infinity; self.attackDamage = 20; self.moveSpeed = 1.0; self.attackCooldown = 100; self.special = "ranged"; break; case 'rifleman': self.attackRange = 500; self.attackDamage = 15; self.moveSpeed = 1.0; self.attackCooldown = 30; // 0.5 seconds at 60 FPS self.special = "ranged"; break; case 'hammer': self.attackRange = 50; self.attackDamage = 30; self.moveSpeed = 1.0; self.attackCooldown = 70; self.special = "aoe"; break; case 'retarius': self.attackRange = 100; self.attackDamage = 18; self.moveSpeed = 1.2; self.attackCooldown = 60; self.special = "stun every 3 attacks"; break; case 'appleThrower': self.attackRange = 300; self.attackDamage = -10; // Negative damage for healing self.moveSpeed = 1.0; self.attackCooldown = 100; self.special = "heals allies"; break; case 'priest': self.attackRange = 250; self.attackDamage = -20; // Heals 20 health self.moveSpeed = 0.8; self.health = 80; self.maxHealth = 80; self.attackCooldown = 80; self.special = "heals allies with prayer"; break; case 'scarecrow': self.attackRange = 250; self.attackDamage = 15; self.moveSpeed = 0.8; self.attackCooldown = 70; self.special = "curses enemies with crows"; self.isLiving = false; // Scarecrows are not living break; case 'warMachine': self.attackRange = 500; self.attackDamage = 30; self.moveSpeed = 0.5; self.health = 300; self.maxHealth = 300; self.attackCooldown = 120; self.special = "area damage, immune to negative effects"; self.isLiving = false; // War machines are not living break; case 'axeThrower': self.attackRange = 500; self.attackDamage = 20; self.moveSpeed = 1.0; self.attackCooldown = 80; self.special = "boomerang attack"; break; case 'jarl': self.attackRange = 90; self.attackDamage = 40; self.moveSpeed = 0.7; self.health = 220; self.maxHealth = 220; self.attackCooldown = 70; self.special = "arena of ice"; break; case 'cannon': self.attackRange = 600; self.attackDamage = 50; self.moveSpeed = 0.4; self.health = 100; self.maxHealth = 100; self.attackCooldown = 150; self.special = "bomb explosion"; self.isLiving = false; // Cannons are not living break; case 'catapult': self.attackRange = 700; self.attackDamage = 40; self.moveSpeed = 0.3; self.health = 120; self.maxHealth = 120; self.attackCooldown = 200; self.special = "splits into smaller rocks"; self.isLiving = false; // Catapults are not living break; case 'romanJavelinThrower': self.attackRange = 400; self.attackDamage = 15; self.moveSpeed = 1.0; self.attackCooldown = 80; self.special = "pierces through 3 enemies"; break; case 'queen': self.attackRange = 300; self.attackDamage = 10; self.moveSpeed = 0.8; self.attackCooldown = 100; self.special = "seduces enemies"; break; case 'swashbuckler': self.attackRange = 60; self.attackDamage = 10; self.moveSpeed = 1.8; self.attackCooldown = 6; // Attack every 0.1 seconds (6 frames at 60 FPS) self.special = "fast attack"; break; case 'pirateCrew': self.attackRange = 400; // Increased range for Pirate Crew self.attackDamage = 8; self.moveSpeed = 1.0; self.attackCooldown = 30; // Fast attack self.special = "ranged"; break; case 'pirateCaptain': self.attackRange = 500; // Increased range for Pirate Captain self.attackDamage = 15; // Stronger flintnock self.moveSpeed = 1.0; self.attackCooldown = 30; // Fast attack self.special = "stronger flintnock and swashbuckle"; break; case 'bombThrower': self.attackRange = 350; self.attackDamage = 25; self.moveSpeed = 0.9; self.health = 80; self.maxHealth = 80; self.attackCooldown = 90; // 1.5 seconds at 60 FPS self.special = "throws bombs that explode after 2 seconds"; break; case 'raptor': self.attackRange = 70; self.attackDamage = 15; self.moveSpeed = 2.0; // Faster than most units self.health = 80; self.maxHealth = 80; self.attackCooldown = 30; // Fast attacks - 0.5 seconds at 60 FPS self.special = "pack hunter"; // More damage when near other raptors self.isLiving = true; break; case 'brachiosaur': self.attackRange = 0; // Cannot attack self.attackDamage = 0; // Pacifist self.moveSpeed = 0.5; // Very slow self.health = 1000; self.maxHealth = 1000; self.attackCooldown = 0; // No attacks self.special = "pacifist"; // Will not attack self.isLiving = true; break; case 't-rex': self.attackRange = 120; self.attackDamage = 50; // High base damage self.moveSpeed = 1.2; self.health = 400; self.maxHealth = 400; self.attackCooldown = 90; // 1.5 seconds between attacks self.special = "anti-machine"; // Does even more damage to non-living units self.isLiving = true; break; case 't-rexJr': self.attackRange = 60; self.attackDamage = 25; // Half of T-rex self.moveSpeed = 0.6; // Half of T-rex self.health = 200; self.maxHealth = 200; self.attackCooldown = 45; // Half of T-rex self.special = "junior anti-machine"; self.isLiving = true; break; case 'robotBlaster': self.attackRange = 500; self.attackDamage = 20; // Plasma rifle damage self.moveSpeed = 1.0; self.health = 150; self.maxHealth = 150; self.attackCooldown = 40; // Fast shooting self.special = "plasma rifle"; // Shoots plasma that disintegrates enemies self.isLiving = false; // Robots are not living break; case 'explodingSpiderBot': self.attackRange = 50; // Needs to get close to explode self.attackDamage = 100; // High damage on explosion self.moveSpeed = 1.5; // Fast movement self.health = 1; // Very fragile self.maxHealth = 1; self.attackCooldown = 0; // No regular attacks self.special = "self-destruct"; // Charges and explodes self.isLiving = false; // Robots are not living break; case 'plane': self.attackRange = 0; // Doesn't target directly, drops bombs self.attackDamage = 50; // Damage of the pineapple explosion self.moveSpeed = 2.0; // Flies reasonably fast self.health = 200; self.maxHealth = 200; self.attackCooldown = 180; // Drops pineapple every 3 seconds self.special = "drops exploding pineapples"; self.isLiving = false; // Planes are not living // Movement specific properties self.targetX = Math.random() * battlefield.width; self.targetY = Math.random() * (battlefield.height * 0.5); // Fly in the upper half break; } // Create unit appearance var unitShape = self.attachAsset(type, { anchorX: 0.5, anchorY: 0.5 }); // Add team color indicator on top var teamIndicator = self.attachAsset('character' + team.charAt(0).toUpperCase() + team.slice(1), { anchorX: 0.5, anchorY: 0.5, width: 20, height: 20 }); teamIndicator.y = -40; // Add health bar self.healthBarBg = self.attachAsset('healthBarBackground', { anchorX: 0.5, anchorY: 0.5 }); self.healthBarBg.y = 40; self.healthBarFill = self.attachAsset('healthBar', { anchorX: 0, // Left anchored for easy scaling anchorY: 0.5 }); self.healthBarFill.x = -35; // Half the width self.healthBarFill.y = 40; self.updateHealthBar = function () { var healthPercent = self.health / self.maxHealth; self.healthBarFill.scaleX = healthPercent; }; self.applyBuff = function (buffType, source) { self.clearBuff(); // Clear any existing buffs first self.buffType = buffType; self.buffSource = source; if (buffType === 'acidicDip') { self.buffAttackCount = 5; self.buffDamageMultiplier = 1.3; // 30% more damage self.buffAttackSpeedMultiplier = 0.7; // 30% faster attacks (70% of original cooldown) self.buffNonLivingBonus = 10; // +10 damage vs non-living console.log(self.type + " received Acidic Dip buff!"); } // Add other buff types here if needed }; self.clearBuff = function () { if (self.buffType) { console.log(self.type + " buff " + self.buffType + " expired."); } self.buffType = null; self.buffAttackCount = 0; self.buffSource = null; self.buffDamageMultiplier = 1.0; self.buffAttackSpeedMultiplier = 1.0; self.buffNonLivingBonus = 0; }; self.takeDamage = function (amount, attacker) { // General flying defense: Flying units are immune to non-flying melee attackers if (self.flying && attacker && !attacker.flying && attacker.attackType === 'melee') { return; // Immune to ground melee } // Plane defense logic: Only take damage from ranged attackers or Valkyries if (self.type === 'plane' && attacker) { var rangedAttackers = ['archer', 'ballista', 'apprentice', 'jeep', 'gatlingGunner', 'mortar', 'crossbowMan', 'sniper', 'rifleman', 'appleThrower', 'wizard', 'witch', 'axeThrower', 'cannon', 'catapult', 'romanJavelinThrower', 'pirateCrew', 'pirateCaptain', 'bombThrower', 'robotBlaster', 'plane' // Pineapples can hurt other planes ]; // Plane defense logic continued if (!rangedAttackers.includes(attacker.type)) { // If attacker is not ranged, plane takes no damage return; } } // Shield units take less damage from archers if (self.type === 'shield' && attacker && attacker.type === 'archer') { amount = Math.floor(amount * 0.5); } // Ensure Rifleman, Roman Legionnaire, and Witch lose health if (self.type === 'rifleman' || self.type === 'romanLegionnaire' || self.type === 'witch') { self.health -= amount; } // Wizards do extra damage to shields if (attacker && attacker.type === 'wizard' && self.type === 'shield') { amount = Math.floor(amount * 1.5); } if (amount > 0) { self.health -= amount; } self.updateHealthBar(); // Flash unit when hit LK.effects.flashObject(self, 0xff0000, 300); if (self.health <= 0) { self.die(); } }; self.die = function () { self.active = false; // Play 'Destroyed' sound for mechanical units, including plane if (self.type === 'warMachine' || self.type === 'cannon' || self.type === 'catapult' || self.type === 'jeep' || self.type === 'ballista' || self.type === 'plane') { LK.getSound('Destroyed').play(); } else if (self.type === 'bombThrower' || self.type === 'chicken') { // Play both die and explosion sounds simultaneously for bombThrower and chicken if (self.type === 'chicken') { LK.getSound('Bawk').play(); } else { LK.getSound('death').play(); } LK.getSound('Explosion').play(); } else if (self.type === 'queen' || self.type === 'witch') { LK.getSound('Girldeath').play(); } else if (self.type === 'raptor' || self.type === 't-rex' || self.type === 't-rexJr' || self.type === 'brachiosaur') { // Dinosaur death sounds - use RAWR sound for all dinosaurs LK.getSound('RAWR').play(); } else if (self.type === 'vikingBearRider') { // Spawn Hammer Viking on death var hammerViking = new Unit(self.team, 'hammer', self.x, self.y); units.push(hammerViking); game.addChild(hammerViking); // Play bear death sound LK.getSound('RAWR').play(); } else if (self.type === 'robotBlaster' || self.type === 'explodingSpiderBot') { LK.getSound('Malfunction').play(); } else { LK.getSound('death').play(); } // Fade out and remove from battlefield tween(self, { alpha: 0 }, { duration: 500, onFinish: function onFinish() { battlefield.removeUnit(self); self.destroy(); } }); }; self.findNearestEnemy = function () { var closestDist = Infinity; var closestEnemy = null; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.team) { // Check if 'self' (the attacker) is a ground melee unit // and the potential 'unit' (target) is flying. // Ground melee units should not target flying units. // Valkyries are an exception (can hit planes), but this logic primarily prevents ground units targeting flyers. // Note: Assumes melee units have `attackType === 'melee'` defined. var isSelfGroundMelee = !self.flying && self.attackType === 'melee'; if (isSelfGroundMelee && unit.flying) { continue; // Skip this flying unit as a target for ground melee } var dist = getDistance(self.x, self.y, unit.x, unit.y); if (dist < closestDist) { closestDist = dist; closestEnemy = unit; } } } return closestEnemy; }; self.findNearestAlly = function () { var includeSelf = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var needsBuff = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var closestDist = Infinity; var closestAlly = null; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team === self.team && (includeSelf || unit !== self)) { // Optionally check if the ally doesn't already have a buff if (needsBuff && unit.buffType !== null) { continue; } var dist = getDistance(self.x, self.y, unit.x, unit.y); if (dist < closestDist) { closestDist = dist; closestAlly = unit; } } } return closestAlly; }; self.findNearestInjuredAlly = function () { var closestDist = Infinity; var mostInjuredAlly = null; var lowestHealthRatio = 1.0; // Start with full health ratio for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team === self.team && unit !== self && unit.health < unit.maxHealth) { var currentHealthRatio = unit.health / unit.maxHealth; var dist = getDistance(self.x, self.y, unit.x, unit.y); // Prioritize closer allies if health ratios are similar, otherwise prioritize most injured // Or maybe just find the closest injured ally within range? Let's try that first. if (dist < closestDist) { closestDist = dist; mostInjuredAlly = unit; } // Alternative: Find the *most* injured ally regardless of distance (within reason?) /* if (currentHealthRatio < lowestHealthRatio) { lowestHealthRatio = currentHealthRatio; mostInjuredAlly = unit; closestDist = dist; // Keep track of distance for tie-breaking maybe } else if (currentHealthRatio === lowestHealthRatio && dist < closestDist) { // Tie-breaker: choose the closer one if equally injured mostInjuredAlly = unit; closestDist = dist; } */ } } // Ensure the target is within attack range if we care about that here // if (mostInjuredAlly && getDistance(self.x, self.y, mostInjuredAlly.x, mostInjuredAlly.y) > self.attackRange) { // return null; // Target out of range // } return mostInjuredAlly; }; self.moveTowardsTarget = function () { if (!self.target || !self.target.active) { if (self.type === 'spearman' && Math.random() < 0.3) { // 30% chance to throw spear self.target = self.findNearestEnemy(); if (self.target) { battlefield.createProjectile(self, self.target, 'javelin'); self.attackCooldown = 60; // Reset cooldown after throwing spear return; } } self.target = self.findNearestEnemy(); if (!self.target) { return; } } if (!self.target) { return; } var dx = self.target.x - self.x; var dy = self.target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); // If in attack range, stop moving if (dist <= self.attackRange) { if (self.attackCooldown === 0) { self.attack(); } return; } // Normalize direction and move var moveX = dx / dist * self.moveSpeed; var moveY = dy / dist * self.moveSpeed; self.x += moveX; self.y += moveY; }; self.attack = function () { // Track number of attacks for Retarius if (self.type === 'retarius') { if (!self.attackCount) { self.attackCount = 0; } self.attackCount++; // Stun effect every 3 attacks if (self.attackCount % 3 === 0 && self.target && self.target.type !== 'warMachine') { self.target.stunned = true; LK.setTimeout(function () { if (self.target) { self.target.stunned = false; } }, 2000); // Stun duration of 2 seconds } } // Chicken special behavior - explode when seeing enemy if (self.type === 'chicken' && self.target && self.active) { var dist = getDistance(self.x, self.y, self.target.x, self.target.y); if (dist <= self.attackRange) { // Explode and damage nearby enemies LK.getSound('Explosion').play(); // Create explosion visual effect var explosion = new Container(); var explosionGraphics = explosion.attachAsset('bomb', { anchorX: 0.5, anchorY: 0.5, width: 200, height: 200, color: 0xFF5500 }); explosion.x = self.x; explosion.y = self.y; explosion.alpha = 0.8; battlefield.addChild(explosion); // Fade out explosion tween(explosion, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 500, onFinish: function onFinish() { explosion.destroy(); } }); // Deal damage to nearby enemy units for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.team) { var distToUnit = getDistance(self.x, self.y, unit.x, unit.y); if (distToUnit <= 200) { // Damage falls off with distance var damageMultiplier = 1 - distToUnit / 200; var damage = Math.floor(self.attackDamage * damageMultiplier); unit.takeDamage(damage, self); } } } // Kill the chicken self.die(); return; } } if (self.buffType && self.buffAttackCount <= 0) { self.clearBuff(); } // Check for Alchemist first if (self.type === 'alchemist') { // Decide which potion to brew (randomly for now) var potionChoice = Math.floor(Math.random() * 3); // 0, 1, or 2 var currentTarget = null; var potionType = ''; if (potionChoice === 0) { // Regular Potion (damage enemy) currentTarget = self.findNearestEnemy(); potionType = 'regularPotion'; if (currentTarget && getDistance(self.x, self.y, currentTarget.x, currentTarget.y) <= self.attackRange) { battlefield.createProjectile(self, currentTarget, potionType); LK.getSound('attack').play(); // Generic attack sound for throwing } else { // No valid enemy target in range, maybe try another potion next time? // Or just reset cooldown without attacking self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier); return; } } else if (potionChoice === 1) { // Heal Potion (heal ally) currentTarget = self.findNearestInjuredAlly(); potionType = 'healPotion'; // Target the ally's current position for the heal AoE center if (currentTarget && getDistance(self.x, self.y, currentTarget.x, currentTarget.y) <= self.attackRange) { // Create a temporary target object for the projectile system var healTargetLocation = { x: currentTarget.x, y: currentTarget.y, active: true, team: self.team }; // Needs team? battlefield.createProjectile(self, healTargetLocation, potionType); LK.getSound('attack').play(); // Generic attack sound } else { // No injured ally in range, reset cooldown self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier); return; } } else { // Acidic Mixture Dip (buff ally) currentTarget = self.findNearestAlly(false, true); // Find nearest ally excluding self that needs a buff potionType = 'acidicMixturePotion'; if (currentTarget && getDistance(self.x, self.y, currentTarget.x, currentTarget.y) <= self.attackRange) { battlefield.createProjectile(self, currentTarget, potionType); LK.getSound('attack').play(); // Generic attack sound } else { // No ally to buff in range, reset cooldown self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier); return; } } // Reset Alchemist's cooldown (applies buff speed multiplier if alchemist is buffed) // Need to store max cooldown somewhere, let's assume it's the initial value from constructor if (!self.maxAttackCooldown) { self.maxAttackCooldown = self.attackCooldown; } // Store on first attack if not set self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier); return; // Alchemist logic finished } // Existing target check for other units if (!self.target || !self.target.active) { // Handle non-Alchemist targeting (e.g., healers finding allies) if (self.type === 'appleThrower' || self.type === 'priest') { self.target = self.findNearestInjuredAlly(); } else { self.target = self.findNearestEnemy(); } if (!self.target) { return; } } // Brachiosaur is a pacifist and doesn't attack if (self.type === 'brachiosaur') { return; } if (self.attackCooldown === 0) { // Ensure attack is only called once per cooldown cycle self.attackCooldown = self.type === 'wobbler' ? 40 : self.type === 'farmer' ? 60 : self.type === 'archer' ? 80 : self.type === 'shield' ? 50 : self.type === 'sword' ? 45 : self.type === 'jeep' ? 20 : self.type === 'raptor' ? 30 : self.type === 't-rex' ? 90 : // Set Jeep cooldown to 0.333 seconds (20 frames) 100; self.attack(); } var dist = self.target ? getDistance(self.x, self.y, self.target.x, self.target.y) : Infinity; if (self.target && dist <= self.attackRange) { // Consume buff charge if attacking if (self.buffType && self.buffAttackCount > 0 && self.target.team !== self.team) { // Only consume on damaging attacks self.buffAttackCount--; console.log(self.type + " buff attacks remaining: " + self.buffAttackCount); if (self.buffAttackCount <= 0) { // Schedule buff removal after this attack resolves LK.setTimeout(function () { self.clearBuff(); }, 10); // Short delay } } // Calculate damage based on unit type var damage = self.attackDamage; // Apply buff damage multiplier damage *= self.buffDamageMultiplier; // Apply buff non-living bonus if (self.buffNonLivingBonus > 0 && self.target && !self.target.isLiving) { damage += self.buffNonLivingBonus; } // Raptor pack bonus: increase damage when near other raptors if (self.type === 'raptor') { var nearbyRaptors = 0; for (var i = 0; i < units.length; i++) { if (units[i].active && units[i].type === 'raptor' && units[i].team === self.team && units[i] !== self) { var raptorDist = getDistance(self.x, self.y, units[i].x, units[i].y); if (raptorDist < 200) { nearbyRaptors++; } } } // 20% damage boost per nearby raptor damage += Math.floor(damage * 0.2 * nearbyRaptors); } // T-Rex does extra damage to non-living things if (self.type === 't-rex' && self.target && !self.target.isLiving) { damage = Math.floor(damage * 1.5); // 50% more damage to non-living units } // Ensure all units deal damage to enemies if (self.target && self.target.team !== self.team) { self.target.takeDamage(damage, self); } // Reset attack cooldown self.attackCooldown = self.type === 'wobbler' ? 40 : self.type === 'farmer' ? 60 : self.type === 'archer' ? 80 : self.type === 'shield' ? 50 : self.type === 'sword' ? 45 : self.type === 'jeep' ? 20 : self.type === 'raptor' ? 30 : self.type === 't-rex' ? 90 : // Set Jeep cooldown to 0.333 seconds (20 frames) 100; // Store max cooldown if not already set (needed for buff calculation) if (!self.maxAttackCooldown) { self.maxAttackCooldown = self.attackCooldown; } // Apply buff speed multiplier self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier); // Handle special attacks if (self.type === 'appleThrower') { // Apple thrower finds injured ally instead of enemy var lowestHealth = Infinity; var allyToHeal = null; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team === self.team && unit !== self && unit.health < unit.maxHealth) { var distToAlly = getDistance(self.x, self.y, unit.x, unit.y); if (distToAlly <= self.attackRange && unit.health < lowestHealth) { lowestHealth = unit.health; allyToHeal = unit; } } } if (allyToHeal) { // Throw apple at ally battlefield.createProjectile(self, allyToHeal, 'apple'); allyToHeal.health = Math.min(allyToHeal.maxHealth, allyToHeal.health + Math.abs(self.attackDamage)); allyToHeal.updateHealthBar(); LK.effects.flashObject(allyToHeal, 0x00ff00, 300); } } else if (self.special === "ranged") { if (self.type === 'pirateCaptain') { LK.getSound('Shoot').play(); } if (self.type === 'pirateCrew') { LK.getSound('Shoot').play(); } if (self.type === 'rifleman') { LK.getSound('Shoot').play(); } if (self.type === 'sniper') { LK.getSound('Shoot').play(); } // Create arrow projectile battlefield.createProjectile(self, self.target, 'arrow'); LK.getSound('arrow').play(); LK.getSound('Shoot').play(); } else if (self.special === "fast but inaccurate shooting") { // Gatling Gunner shoots 25 bullets per second aiming at the target with a chance to miss for (var i = 0; i < 25; i++) { var angleToTarget = Math.atan2(self.target.y - self.y, self.target.x - self.x); var missChance = Math.random() * 0.2 - 0.1; // Random miss angle between -0.1 and 0.1 radians var bullet = new Projectile(self, null, 'bullet'); bullet.x = self.x; bullet.y = self.y; bullet.rotation = angleToTarget + missChance; bullet.speed = 12; // Set bullet speed bullet.update = function () { this.x += Math.cos(this.rotation) * this.speed; this.y += Math.sin(this.rotation) * this.speed; // Check if bullet is out of bounds if (this.x < 0 || this.x > 2048 || this.y < 0 || this.y > 2732) { this.destroy(); } }; battlefield.addChild(bullet); battlefield.projectiles.push(bullet); } LK.getSound('attack').play(); } else if (self.special === "vehicle, machine gun") { // Jeep fires ONE bullet every 0.333 seconds var angleToTarget = Math.atan2(self.target.y - self.y, self.target.x - self.x); // Create a single bullet with no spread for accuracy var bullet = new Projectile(self, null, 'bullet'); bullet.x = self.x; bullet.y = self.y; bullet.rotation = angleToTarget; bullet.speed = 13; bullet.update = function () { this.x += Math.cos(this.rotation) * this.speed; this.y += Math.sin(this.rotation) * this.speed; // Check if bullet is out of bounds if (this.x < 0 || this.x > 2048 || this.y < 0 || this.y > 2732) { this.destroy(); } }; battlefield.addChild(bullet); battlefield.projectiles.push(bullet); LK.getSound('Shoot').play(); } else if (self.special === "bomb explosion") { // Cannon bomb explosion battlefield.createProjectile(self, self.target, 'bomb'); LK.getSound('Explosion').play(); var damage = self.attackDamage; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.team) { var aoeDistance = getDistance(self.target.x, self.target.y, unit.x, unit.y); if (aoeDistance < 200) { var aoeDamage = Math.floor(damage * (1 - aoeDistance / 200)); if (aoeDamage > 0) { unit.takeDamage(aoeDamage, self); } } } } } else if (self.special === "splits into smaller rocks") { // Catapult attack logic battlefield.createProjectile(self, self.target, 'rock'); LK.getSound('Explosion').play(); } else if (self.special === "pierces through 3 enemies") { battlefield.createProjectile(self, self.target, 'javelin'); LK.getSound('arrow').play(); } else if (self.special === "magic ball") { battlefield.createProjectile(self, self.target, 'spell'); LK.getSound('spell').play(); } else if (self.special === "seduces enemies") { // Queen's seduction ability if (Math.random() < 0.1 && self.target.type !== 'warMachine' && self.target.type !== 'king') { self.target.team = 'blue'; // Change team to blue self.target.updateHealthBar(); // Update health bar to reflect team change self.target.findNearestEnemy(); // Reassign target to find new enemies self.target.target = null; // Stop attacking the Queen self.target = null; // Queen stops attacking the seduced enemy LK.getSound('Kiss').play(); // Play 'Kiss' sound when Queen seduces an enemy } // Wizard AOE attack battlefield.createProjectile(self, self.target, 'spell'); if (self.type === 'wizard') { LK.getSound('spell').play(); } else { LK.getSound('Explosion').play(); } // Damage the target and nearby enemies var damage = self.attackDamage; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.team) { if (self.target) { var aoeDistance = getDistance(self.target.x, self.target.y, unit.x, unit.y); } if (aoeDistance < 150) { // Damage falls off with distance var aoeDamage = Math.floor(damage * (1 - aoeDistance / 150)); if (aoeDamage > 0) { unit.takeDamage(aoeDamage, self); } } } } } else if (self.special === "throws bombs that explode after 2 seconds") { // Bomb thrower attack var bomb = new Bomb(self, self.target); battlefield.addChild(bomb); battlefield.projectiles.push(bomb); LK.getSound('attack').play(); } else if (self.special === "curses enemies with crows") { if (!self.attackCount) { self.attackCount = 0; } self.attackCount++; if (self.attackCount % 3 === 0) { // Scarecrow shoots crows every 3 attacks battlefield.createProjectile(self, self.target, 'crow'); LK.getSound('Gnehehe').play(); // Reduce target's attack by 5 points if (self.target) { self.target.attackDamage = Math.max(0, self.target.attackDamage - 5); } } // Melee attack LK.getSound('attack').play(); var damage = self.attackDamage; // Viking Bear Rider AoE damage if (self.type === 'vikingBearRider' && self.target) { var aoeRadius = 100; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.team && unit !== self.target) { // Don't hit primary target twice with AoE var aoeDist = getDistance(self.target.x, self.target.y, unit.x, unit.y); // AoE centered on target if (aoeDist <= aoeRadius) { var aoeDamageMultiplier = 1 - aoeDist / aoeRadius; // Damage falls off var finalAoeDamage = Math.floor(damage * 0.5 * aoeDamageMultiplier); // AoE deals 50% base damage max if (finalAoeDamage > 0) { unit.takeDamage(finalAoeDamage, self); } } } } // Play bear attack sound LK.getSound('RAWR').play(); } // Double damage if Berserker is enraged (health is at or below half) if (self.type === 'berserker' && self.health <= self.maxHealth / 2) { damage *= 2; self.moveSpeed = 2.0; // Increase move speed when enraged } // Implement critical hit chance for Crossbowman if (self.type === 'crossbowMan' && Math.random() < 0.2) { // 20% chance for critical hit damage *= 2; // Double damage for critical hit } // First strike bonus for farmers if (self.special === "firstStrike" && !self.hasAttacked) { damage *= 1.5; // 50% bonus on first hit self.hasAttacked = true; } self.target.takeDamage(damage, self); } else if (self.special === "plasma rifle") { // Robot Blaster attack logic battlefield.createProjectile(self, self.target, 'bullet'); LK.getSound('Blaster').play(); } else if (self.special === "self-destruct") { // Exploding Spider Bot attack logic if (getDistance(self.x, self.y, self.target.x, self.target.y) <= self.attackRange) { self.target.takeDamage(self.attackDamage, self); self.die(); // Self-destruct after attacking } } else if (self.special === "big arrows with long range") { // Ballista attack logic // Create big arrow projectile var bigArrow = new Projectile(self, self.target, 'bigArrow'); bigArrow.x = self.x; bigArrow.y = self.y; bigArrow.speed = 18; // Faster than normal arrows bigArrow.damage = self.attackDamage; battlefield.addChild(bigArrow); battlefield.projectiles.push(bigArrow); LK.getSound('arrow').play(); } else if (self.special === "weak magic") { // Apprentice attack logic battlefield.createProjectile(self, self.target, 'spell'); LK.getSound('spell').play(); // Deal smaller splash damage to target area var damage = self.attackDamage; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.team) { var aoeDistance = getDistance(self.target.x, self.target.y, unit.x, unit.y); if (aoeDistance < 100) { // Smaller area than full wizard var aoeDamage = Math.floor(damage * (1 - aoeDistance / 100)); if (aoeDamage > 0) { unit.takeDamage(aoeDamage, self); } } } } } else if (self.type === "priest") { // Priest healing logic: heal the most injured ally in range var lowestHealth = Infinity; var allyToHeal = null; for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team === self.team && unit !== self && unit.health < unit.maxHealth) { var distToAlly = getDistance(self.x, self.y, unit.x, unit.y); if (distToAlly <= self.attackRange && unit.health < lowestHealth) { lowestHealth = unit.health; allyToHeal = unit; } } } if (allyToHeal) { allyToHeal.health = Math.min(allyToHeal.maxHealth, allyToHeal.health + 20); allyToHeal.updateHealthBar(); LK.effects.flashObject(allyToHeal, 0x00ffcc, 400); LK.getSound('Holy').play(); // Play holy sound when priest heals ally } } else if (self.type === "jarl") { // Jarl's arena of ice: trap the target and nearby enemies for 2 seconds if (self.target && getDistance(self.x, self.y, self.target.x, self.target.y) <= self.attackRange) { for (var i = 0; i < units.length; i++) { var unit = units[i]; if (unit.active && unit.team !== self.team) { var distToArena = getDistance(self.target.x, self.target.y, unit.x, unit.y); if (distToArena < 120) { unit.stunned = true; LK.effects.flashObject(unit, 0x99e6ff, 600); (function (unitRef) { LK.setTimeout(function () { if (unitRef) { unitRef.stunned = false; } }, 2000); })(unit); } } } } } // This is now handled within the plane's update logic, but we need the attack action itself } else if (self.type === 'plane') { // This function is called when the plane is in position and ready to attack // Plane drops a pineapple below itself var pineapple = new Pineapple(self); pineapple.x = self.x; // Ensure it drops exactly below the plane pineapple.y = self.y; battlefield.addChild(pineapple); // Add to projectiles array for management battlefield.projectiles.push(pineapple); // Reset cooldown AFTER attacking self.attackCooldown = 180; // 3 seconds cooldown remains correct // No return needed here as the calling logic in update handles flow } }; self.showStats = function () { var statsText = "Type: " + self.type.charAt(0).toUpperCase() + self.type.slice(1) + "\n" + "Health: " + self.health + "/" + self.maxHealth + "\n" + "Attack Damage: " + self.attackDamage + "\n" + "Attack Range: " + self.attackRange + "\n" + "Living: " + (self.isLiving ? "Yes" : "No") + "\n" + "Special: " + (self.special ? self.special : "None"); var statsDisplay = new Text2(statsText, { size: 30, fill: 0xFFFFFF }); statsDisplay.anchor.set(0.5, 0.5); statsDisplay.x = self.x; statsDisplay.y = self.y - 100; game.addChild(statsDisplay); // Remove stats display after 3 seconds LK.setTimeout(function () { if (statsDisplay.parent) { statsDisplay.destroy(); } }, 3000); }; self.down = function (x, y, obj) { if (self.lastTap && Date.now() - self.lastTap < 300) { self.showStats(); } self.lastTap = Date.now(); }; self.update = function () { if (!self.active || self.stunned) { return; } // Roman Legionnaire invincibility logic if (self.type === 'romanLegionnaire') { if (!self.invincibilityCooldown) { self.invincibilityCooldown = 300; // 5 seconds at 60 FPS } if (self.invincibilityCooldown > 0) { self.invincibilityCooldown--; } if (self.invincibilityCooldown === 0) { self.invincible = true; LK.getSound('Defend').play(); // Play 'Defend' sound LK.setTimeout(function () { self.invincible = false; self.invincibilityCooldown = 300; // Reset cooldown }, 1000); // Invincibility duration of 1 second } } // Update health bar position to follow unit self.updateHealthBar(); // Decrease attack cooldown if (self.attackCooldown > 0) { self.attackCooldown--; } // Find and move towards target if (self.type === 'plane') { // Plane specific logic: Find enemy, fly towards, drop bomb, fly away/retarget if (!self.target || !self.target.active) { self.target = self.findNearestEnemy(); } if (self.target) { var dx = self.target.x - self.x; // Aim slightly above the target's head maybe? var targetFlyY = self.target.y - 100; // Fly 100px above target var dy = targetFlyY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); // If close enough and cooldown ready, attack (drop pineapple) // Let's define "close enough" as within 50 pixels horizontally or vertically if (Math.abs(dx) < 50 && Math.abs(dy) < 50 && self.attackCooldown <= 0 && battlefield.initialized) { self.attack(); // Drop pineapple self.target = null; // Find a new target after dropping } else { // Move towards target // Normalize direction and move var moveX = dx / dist * self.moveSpeed; var moveY = dy / dist * self.moveSpeed; // Ensure plane stays roughly in the upper half of the battlefield if (self.y + moveY < battlefield.height * 0.7 || moveY < 0) { self.x += moveX; self.y += moveY; } else { // If moving down would take it too low, just move horizontally self.x += moveX; // Optionally, try to find a new target if stuck low if (self.y > battlefield.height * 0.65) { self.target = null; } } // Optional: Rotate plane towards movement direction // self.rotation = Math.atan2(moveY, moveX); } } else { // No target, fly randomly for a bit? Or towards center? // Let's fly towards a random point in the upper half if (!self.randomTargetX || getDistance(self.x, self.y, self.randomTargetX, self.randomTargetY) < self.moveSpeed * 5) { self.randomTargetX = Math.random() * battlefield.width; self.randomTargetY = (Math.random() * 0.6 + 0.1) * battlefield.height; // Upper 60% } var dxRand = self.randomTargetX - self.x; var dyRand = self.randomTargetY - self.y; var distRand = Math.sqrt(dxRand * dxRand + dyRand * dyRand); if (distRand > self.moveSpeed) { self.x += dxRand / distRand * self.moveSpeed; self.y += dyRand / distRand * self.moveSpeed; } } } else { // Original logic for other units self.moveTowardsTarget(); } if (self.attackCooldown === 0 && battlefield.initialized) { // Necromancer summons skeletons every 1.5 seconds if battle has started if (self.type === 'necromancer' && battlefield.initialized && LK.ticks % 150 === 0) { // Count current skeletons on the battlefield var skeletonCount = units.filter(function (unit) { return unit.type === 'skeleton' && unit.active; }).length; if (skeletonCount < 3) { var skeleton = new Unit(self.team, 'skeleton', self.x, self.y); units.push(skeleton); game.addChild(skeleton); LK.getSound('Gnehehe').play(); } } // self.attack(); // Removed: Attack logic is now handled within moveTowardsTarget or plane specific update logic } }; return self; }); var UnitButton = Container.expand(function (team, type, x, y) { var self = Container.call(this); self.team = team; self.type = type; self.x = x; self.y = y; // Create button appearance var buttonShape = self.attachAsset('button' + team.charAt(0).toUpperCase() + team.slice(1), { anchorX: 0.5, anchorY: 0.5, width: 150, height: 80 }); // Add unit icon var unitIcon = self.attachAsset(type, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8 }); // Add lock icon if unit is locked var lockIcon = null; if (endlessMode.active && !endlessMode.unlockedUnits.includes(self.type)) { lockIcon = new Text2("🔒", { size: 24, fill: 0xFF0000 }); lockIcon.anchor.set(0.5, 0.5); lockIcon.y = -30; self.addChild(lockIcon); } // Remove lock icon if unit is unlocked self.updateLockIcon = function () { if (endlessMode.unlockedUnits.includes(self.type) && lockIcon && lockIcon.parent) { lockIcon.destroy(); lockIcon = null; } }; self.updateLockIcon(); // Add unit name text var nameText = new Text2(type.charAt(0).toUpperCase() + type.slice(1), { size: 24, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0.5); nameText.y = 30; self.addChild(nameText); self.down = function (x, y, obj) { // When button is pressed, start dragging a new unit if (endlessMode.active && !endlessMode.unlockedUnits.includes(self.type)) { return; // Prevent adding locked units } var unit = new Unit(self.team, self.type, x, y); // Mark as dragging unit draggingUnit = unit; game.addChild(unit); // Highlight button LK.effects.flashObject(self, 0xffffff, 300); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ // Light green, semi-transparent // Lime green, semi-transparent // Yellow // Bright green // Dark red/brown // Define factions and their respective units var endlessModeInstructions = new Text2("Endless Mode Instructions:\n- Place units on the battlefield.\n- Survive waves of enemies.\n- Unlock new units as you progress.\n- Win by unlocking all units.", { size: 30, fill: 0xFFFFFF }); endlessModeInstructions.anchor.set(0.5, 0.5); endlessModeInstructions.x = 2048 / 2; endlessModeInstructions.y = 2732 / 2 - 200; endlessModeInstructions.visible = false; game.addChild(endlessModeInstructions); var endlessMode = { active: false, currentWave: 0, unlockedUnits: ['wobbler'], enemyUnits: ['farmer', 'archer', 'sword', 'shield', 'berserker', 'spearman', 'retarius', 'romanJavelinThrower', 'romanLegionnaire', 'wizard', 'witch', 'necromancer', 'skeleton', 'mirrorShield', 'crossbowMan', 'sniper', 'warMachine', 'rifleman', 'hammer', 'axeThrower', 'cannon', 'catapult', 'queen', 'raptor', 'brachiosaur', 't-rex'], // The robots faction will be added here when units are implemented start: function start() { var _this2 = this; this.active = true; this.currentWave = 0; this.unlockedUnits = ['wobbler']; this.lockedUnits = Object.keys(factions).reduce(function (acc, faction) { return acc.concat(factions[faction].filter(function (unit) { return !_this2.unlockedUnits.includes(unit); })); }, []); this.spawnNextWave(); }, spawnNextWave: function spawnNextWave() { this.currentWave++; var numEnemies = this.currentWave * 2; // Increase number of enemies with each wave for (var i = 0; i < numEnemies; i++) { var enemyType = this.enemyUnits[Math.floor(Math.random() * this.enemyUnits.length)]; // Count current units of the same type on the battlefield var currentUnitCount = units.filter(function (unit) { return unit.type === enemyType && unit.team === 'red' && unit.active; }).length; // Limit the number of units based on type var maxUnits = enemyType === 'wobbler' ? 5 : 3; if (currentUnitCount < maxUnits) { var enemy = new Unit('red', enemyType, Math.random() * 2048, Math.random() * 1366); units.push(enemy); game.addChild(enemy); } } if (units.filter(function (unit) { return unit.team === 'red' && unit.active; }).length === 0) { this.unlockUnit(); } else { return; // Exit if there are still active red units } }, unlockUnit: function unlockUnit() { var _this = this; if (this.lockedUnits.length > 0) { var nextUnit = this.lockedUnits[Math.floor(Math.random() * this.lockedUnits.length)]; if (nextUnit) { this.unlockedUnits.push(nextUnit); this.lockedUnits = this.lockedUnits.filter(function (unit) { return unit !== nextUnit; }); // Display popup for unlocked unit var popupText = new Text2("Unlocked: " + nextUnit.charAt(0).toUpperCase() + nextUnit.slice(1), { size: 50, fill: 0xFFFFFF }); popupText.anchor.set(0.5, 0.5); popupText.x = 2048 / 2; popupText.y = 2732 / 2; game.addChild(popupText); // Remove popup after 2 seconds LK.setTimeout(function () { if (popupText.parent) { popupText.destroy(); } }, 2000); } } }, checkWinCondition: function checkWinCondition() { if (this.unlockedUnits.length === Object.keys(factions).reduce(function (acc, faction) { return acc.concat(factions[faction]); }, []).length) { LK.showYouWin(); this.active = false; this.unlockedUnits = Object.keys(factions).reduce(function (acc, faction) { return acc.concat(factions[faction]); }, []); } } }; function _typeof2(o) { "@babel/helpers - typeof"; return _typeof2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof2(o); } function _defineProperty3(e, r, t) { return (r = _toPropertyKey2(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey2(t) { var i = _toPrimitive2(t, "string"); return "symbol" == _typeof2(i) ? i : i + ""; } function _toPrimitive2(t, r) { if ("object" != _typeof2(t) || !t) { return t; } var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof2(i)) { return i; } throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) { return t; } var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) { return i; } throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var factions = _defineProperty(_defineProperty(_defineProperty3({ farm: ['wobbler', 'farmer', 'appleThrower', 'scarecrow', 'chicken'], medieval: ['sword', 'archer', 'crossbowMan', 'king', 'shield', 'cannon', 'queen', 'catapult', 'priest'], rome: ['spearman', 'retarius', 'romanJavelinThrower', 'romanLegionnaire', 'ballista'], magic: ['mirrorShield', 'wizard', 'necromancer', 'skeleton', 'witch', 'apprentice', 'alchemist'], vikings: ['berserker', 'hammer', 'axeThrower', 'jarl', 'vikingBearRider'], // Added Viking Bear Rider // Removed Valkyrie modern: ['sniper', 'warMachine', 'rifleman', 'mortar', 'gatlingGunner', 'jeep', 'plane'], pirates: ['swashbuckler', 'pirateCrew', 'pirateCaptain', 'bombThrower'], dinosaurs: ['raptor', 'brachiosaur', 't-rex', 't-rexJr'], // Added dinosaur units robots: ['robotBlaster', 'explodingSpiderBot'] // Added Robot Blaster and Exploding Spider Bot to robots faction }, "rome", ['spearman', 'retarius', 'romanJavelinThrower', 'romanLegionnaire', 'ballista']), "vikings", ['berserker', 'hammer', 'axeThrower', 'jarl', 'vikingBearRider']), "modern", ['sniper', 'warMachine', 'rifleman', 'mortar', 'gatlingGunner', 'jeep', 'plane'], "farm", ['wobbler', 'farmer', 'appleThrower', 'scarecrow', 'chicken']); // Removed Valkyrie, Added Viking Bear Rider // Game state variables var units = []; var draggingUnit = null; var battlefield = null; var resetButton = null; // Create the battlefield battlefield = new Battlefield(); battlefield.x = 0; battlefield.y = 0; game.addChild(battlefield); // Create faction buttons for red team var redFactionButtons = []; var factionNames = Object.keys(factions); var factionYOffset = 80; for (var i = 0; i < factionNames.length; i++) { var factionButton = new FactionButton('red', factionNames[i], 2048 - 300, factionYOffset); factionButton.x = 2048 - 300; // Adjusted position even further to the left factionButton.y = factionYOffset; redFactionButtons.push(factionButton); game.addChild(factionButton); factionYOffset += 100; } // Create faction buttons for blue team var blueFactionButtons = []; factionYOffset = 2732 - 80; for (var i = 0; i < factionNames.length; i++) { var factionButton = new FactionButton('blue', factionNames[i], 60, factionYOffset); blueFactionButtons.push(factionButton); game.addChild(factionButton); factionYOffset -= 100; } // Start button var startButton = new StartButton(); startButton.x = 2048 / 2; startButton.y = 2732 / 2; game.addChild(startButton); // Endless mode button var endlessModeButton = new EndlessModeButton(); endlessModeButton.x = 2048 / 2; endlessModeButton.y = 2732 / 2 + 100; game.addChild(endlessModeButton); // Endless mode instructions button var endlessModeInstructionsButton = new EndlessModeInstructionsButton(); endlessModeInstructionsButton.x = 2048 / 2; endlessModeInstructionsButton.y = 2732 / 2 + 200; game.addChild(endlessModeInstructionsButton); // Win text var winText = new Text2("", { size: 80, fill: 0xFFFF00 }); winText.anchor.set(0.5, 0.5); winText.x = 2048 / 2; winText.y = 2732 / 2 - 100; winText.visible = false; game.addChild(winText); // Instructions text var instructions = new Text2("Place units on the battlefield\nand press Start Battle!", { size: 40, fill: 0xFFFFFF }); instructions.anchor.set(0.5, 0.5); instructions.x = 2048 / 2; instructions.y = 2732 / 2 - 100; game.addChild(instructions); // Helper function to calculate distance between two points function getDistance(x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } // Reset the battle function resetBattle() { // Remove all units for (var i = units.length - 1; i >= 0; i--) { if (units[i].parent) { units[i].destroy(); } } units = []; // Clear projectiles for (var i = battlefield.projectiles.length - 1; i >= 0; i--) { if (battlefield.projectiles[i].parent) { battlefield.projectiles[i].destroy(); } } battlefield.projectiles = []; // Reset battlefield state battlefield.initialized = false; // Show start button, endless mode button, and instructions again if (startButton) { startButton.visible = true; } if (endlessModeButton) { endlessModeButton.visible = true; } if (instructions) { instructions.visible = true; } // Hide win text and reset button if (winText) { winText.visible = false; } if (resetButton && resetButton.parent) { resetButton.destroy(); resetButton = null; } // Stop music LK.stopMusic(); } // Handle dragging game.move = function (x, y, obj) { if (draggingUnit) { draggingUnit.x = x; draggingUnit.y = y; } }; game.up = function (x, y, obj) { if (draggingUnit) { // Check if unit is placed on the battlefield (not on buttons) var validPlacement = y > 150 && y < 2732 - 150; if (validPlacement) { // Add to units array units.push(draggingUnit); } else { // Not valid placement, destroy the unit draggingUnit.destroy(); } draggingUnit = null; } }; // Game update loop game.update = function () { // Update all units if battle has started if (battlefield.initialized) { for (var i = 0; i < units.length; i++) { if (units[i].active) { units[i].update(); } } if (endlessMode.active) { var redTeamUnits = units.filter(function (unit) { return unit.team === 'red' && unit.active; }).length; if (redTeamUnits === 0) { endlessMode.spawnNextWave(); endlessMode.unlockUnit(); } endlessMode.checkWinCondition(); } } // Update projectiles battlefield.update(); };
===================================================================
--- original.js
+++ change.js
@@ -11,22 +11,27 @@
var self = Container.call(this);
self.width = 2048;
self.height = 1366;
self.initialized = false;
+ self.pools = []; // Array to hold active pools
// Create background
var bg = self.attachAsset('background', {
anchorX: 0,
anchorY: 0
});
self.projectiles = [];
- self.pools = []; // Array to store active potion pools
self.createProjectile = function (source, target, type) {
var projectile = new Projectile(source, target, type);
projectile.x = source.x;
projectile.y = source.y;
self.addChild(projectile);
self.projectiles.push(projectile);
};
+ self.createPool = function (type, x, y, sourceTeam) {
+ var pool = new Pool(type, x, y, sourceTeam);
+ self.addChild(pool);
+ self.pools.push(pool);
+ };
self.removeUnit = function (unit) {
var index = units.indexOf(unit);
if (index !== -1) {
units.splice(index, 1);
@@ -81,18 +86,19 @@
self.projectiles.splice(i, 1);
}
}
// Update all pools
- for (var j = self.pools.length - 1; j >= 0; j--) {
- var pool = self.pools[j];
+ for (var i = self.pools.length - 1; i >= 0; i--) {
+ var pool = self.pools[i];
if (!pool || !pool.parent) {
- self.pools.splice(j, 1);
+ // Already destroyed or removed, splice it out
+ self.pools.splice(i, 1);
continue;
}
pool.update();
- // Re-check if the pool was destroyed during its own update
+ // Re-check after update in case it destroyed itself
if (!pool.parent) {
- self.pools.splice(j, 1);
+ self.pools.splice(i, 1);
}
}
};
return self;
@@ -427,48 +433,66 @@
self.destroy(); //{Pineapple_41}
}; //{Pineapple_42}
return self; //{Pineapple_43}
});
-// Class for Acid and Heal Pools
-var PotionPool = Container.expand(function (type, x, y, sourceTeam) {
+var Pool = Container.expand(function (type, x, y, sourceTeam) {
var self = Container.call(this);
- self.poolType = type; // 'acid' or 'heal'
+ self.type = type; // 'acid' or 'healing'
self.x = x;
self.y = y;
- self.sourceTeam = sourceTeam; // Team of the alchemist who created it
- self.duration = 300; // 5 seconds * 60 FPS
- self.tickRate = 30; // Apply effect every 0.5 seconds
- self.tickCooldown = 0;
- var poolAssetId = type === 'acid' ? 'acidPool' : 'healPool';
+ self.sourceTeam = sourceTeam;
+ self.duration = 300; // 5 seconds
+ self.tickInterval = 30; // Apply effect every 0.5 seconds
+ self.tickTimer = self.tickInterval;
+ self.radius = 60; // Half the width/height of the asset
+ var poolAssetId = type === 'acid' ? 'acidPool' : 'healingPool';
var poolGraphics = self.attachAsset(poolAssetId, {
anchorX: 0.5,
anchorY: 0.5,
- alpha: 0.5 // Make it semi-transparent
+ alpha: 0.6
});
self.update = function () {
self.duration--;
- self.tickCooldown--;
- if (self.duration <= 0) {
- // Fade out? Or just destroy. Let's just destroy for now.
- self.destroy();
- return;
- }
- if (self.tickCooldown <= 0) {
+ self.tickTimer--;
+ if (self.tickTimer <= 0) {
self.applyEffect();
- self.tickCooldown = self.tickRate;
+ self.tickTimer = self.tickInterval;
}
+ if (self.duration <= 0) {
+ // Fade out and destroy
+ tween(self, {
+ alpha: 0
+ }, {
+ duration: 300,
+ onFinish: function onFinish() {
+ if (self.parent) {
+ // Find in battlefield pools and remove
+ var index = battlefield.pools.indexOf(self);
+ if (index !== -1) {
+ battlefield.pools.splice(index, 1);
+ }
+ self.destroy();
+ }
+ }
+ });
+ }
};
self.applyEffect = function () {
for (var i = 0; i < units.length; i++) {
var unit = units[i];
- if (unit.active && getDistance(self.x, self.y, unit.x, unit.y) < poolGraphics.width / 2) {
- if (self.poolType === 'acid' && unit.team !== self.sourceTeam) {
- unit.takeDamage(3, null); // Deal 3 DoT damage, source is null to avoid loops/attribution issues?
- LK.effects.flashObject(unit, 0x00ff00, 150); // Flash green slightly
- } else if (self.poolType === 'heal' && unit.team === self.sourceTeam) {
- unit.health = Math.min(unit.maxHealth, unit.health + 2); // Heal 2 HoT
- unit.updateHealthBar();
- LK.effects.flashObject(unit, 0xffc0cb, 150); // Flash pink slightly
+ if (unit.active) {
+ var dist = getDistance(self.x, self.y, unit.x, unit.y);
+ if (dist <= self.radius) {
+ if (self.type === 'acid' && unit.team !== self.sourceTeam) {
+ // Acid damages enemies
+ unit.takeDamage(5, self); // Small DoT damage
+ } else if (self.type === 'healing' && unit.team === self.sourceTeam) {
+ // Healing heals allies
+ if (unit.health < unit.maxHealth) {
+ unit.takeDamage(-8, self); // Heal amount (negative damage)
+ LK.effects.flashObject(unit, 0x00ff00, 150); // Quick green flash
+ }
+ }
}
}
}
};
@@ -514,9 +538,63 @@
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
} else {
// Hit target
- if (type === 'arrow') {
+ // Handle Alchemist potions first
+ if (self.source && self.source.type === 'alchemist') {
+ if (type === 'regularPotion' && self.target) {
+ // Regular Potion: AoE Damage + Acid Pool
+ var explosionRadius = 80;
+ var baseDamage = self.damage; // Use alchemist's damage stat
+ LK.getSound('Explosion').play(); // Use explosion sound? Or a splash?
+ // Deal AoE damage
+ for (var i = 0; i < units.length; i++) {
+ var unit = units[i];
+ if (unit.active && unit.team !== self.source.team) {
+ var dist = getDistance(self.target.x, self.target.y, unit.x, unit.y);
+ if (dist <= explosionRadius) {
+ var damageMultiplier = 1 - dist / explosionRadius;
+ var damage = Math.floor(baseDamage * damageMultiplier);
+ if (damage > 0) {
+ unit.takeDamage(damage, self.source);
+ }
+ }
+ }
+ }
+ // Create Acid Pool
+ battlefield.createPool('acid', self.target.x, self.target.y, self.source.team);
+ } else if (type === 'healPotion' && self.target) {
+ // Heal Potion: AoE Heal + Healing Pool
+ var healRadius = 90;
+ var baseHeal = Math.abs(self.damage); // Use alchemist's damage as heal amount
+ LK.getSound('Holy').play(); // Use holy sound?
+ // Heal nearby allies
+ for (var i = 0; i < units.length; i++) {
+ var unit = units[i];
+ if (unit.active && unit.team === self.source.team) {
+ var dist = getDistance(self.target.x, self.target.y, unit.x, unit.y);
+ if (dist <= healRadius) {
+ var healMultiplier = 1 - dist / healRadius;
+ var healAmount = Math.floor(baseHeal * healMultiplier);
+ if (healAmount > 0 && unit.health < unit.maxHealth) {
+ unit.takeDamage(-healAmount, self.source); // Negative damage to heal
+ LK.effects.flashObject(unit, 0x00ff00, 300);
+ }
+ }
+ }
+ }
+ // Create Healing Pool
+ battlefield.createPool('healing', self.target.x, self.target.y, self.source.team);
+ } else if (type === 'acidicMixturePotion' && self.target && self.target.team === self.source.team) {
+ // Acidic Mixture Dip: Buff Ally
+ self.target.applyBuff('acidicDip', self.source); // Apply buff directly
+ LK.getSound('spell').play(); // Use spell sound?
+ LK.effects.flashObject(self.target, 0xffff00, 500); // Yellow flash
+ }
+ // Destroy potion projectile after effect
+ self.destroy();
+ return; // Potion handling complete
+ } else if (type === 'arrow') {
if (self.target.type === 'mirrorShield' && Math.random() < 0.5) {
// 50% chance to reflect
// Reflect the projectile back to the source
self.target = self.source;
@@ -552,62 +630,8 @@
} // Stop after piercing 3 enemies
}
}
}
- } else if (type === 'regularPotion') {
- // Acid Potion impact
- LK.getSound('Explosion').play(); // Reuse explosion sound? Or need a splash sound
- // Deal AoE damage
- var aoeRadius = 100;
- var baseDamage = 15; // Base damage of potion splash
- for (var i = 0; i < units.length; i++) {
- var unit = units[i];
- // Damage enemies only
- if (unit.active && unit.team !== self.source.team) {
- var distToUnit = getDistance(self.x, self.y, unit.x, unit.y);
- if (distToUnit <= aoeRadius) {
- var damageMultiplier = 1 - distToUnit / aoeRadius;
- var damage = Math.floor(baseDamage * damageMultiplier);
- if (damage > 0) {
- unit.takeDamage(damage, self.source);
- }
- }
- }
- }
- // Create Acid Pool
- var acidPool = new PotionPool('acid', self.x, self.y, self.source.team);
- battlefield.addChild(acidPool);
- battlefield.pools.push(acidPool);
- self.destroy(); // Destroy potion projectile
- return; // Exit update early
- } else if (type === 'healPotion') {
- // Heal Potion impact
- LK.getSound('Holy').play(); // Reuse priest sound?
- // Heal allies in AoE
- var healRadius = 100;
- var baseHeal = 25; // Base heal amount
- for (var i = 0; i < units.length; i++) {
- var unit = units[i];
- // Heal allies only
- if (unit.active && unit.team === self.source.team) {
- var distToUnit = getDistance(self.x, self.y, unit.x, unit.y);
- if (distToUnit <= healRadius) {
- var healMultiplier = 1 - distToUnit / healRadius;
- var healAmount = Math.floor(baseHeal * healMultiplier);
- if (healAmount > 0) {
- unit.health = Math.min(unit.maxHealth, unit.health + healAmount);
- unit.updateHealthBar();
- LK.effects.flashObject(unit, 0x00ff00, 300); // Flash green on heal
- }
- }
- }
- }
- // Create Heal Pool
- var healPool = new PotionPool('heal', self.x, self.y, self.source.team);
- battlefield.addChild(healPool);
- battlefield.pools.push(healPool);
- self.destroy(); // Destroy potion projectile
- return; // Exit update early
}
self.destroy();
}
};
@@ -669,11 +693,15 @@
self.health = 100;
self.maxHealth = 100;
self.special = "";
self.isLiving = true; // New property to categorize living vs non-living units
- self.buff = null; // Track active buffs
- self.remainingBuffAttacks = 0; // Counter for attack-based buffs
- self.baseAttackCooldown = 0; // Store original cooldown for buff restoration
+ // Buff properties
+ self.buffType = null;
+ self.buffAttackCount = 0;
+ self.buffSource = null; // To attribute buff effects if needed
+ self.buffDamageMultiplier = 1.0; // Multiplier for damage dealt
+ self.buffAttackSpeedMultiplier = 1.0; // Multiplier for attack speed (lower cooldown)
+ self.buffNonLivingBonus = 0; // Flat bonus damage vs non-living
// Default properties based on unit type
switch (type) {
case 'ballista':
self.attackRange = 800; // Nearly infinite range
@@ -727,8 +755,20 @@
self.attackCooldown = 20; // Fire every 0.333 seconds (20 frames at 60 FPS)
self.special = "vehicle, machine gun";
self.isLiving = false; // Vehicles are not living
break;
+ case 'alchemist':
+ self.attackRange = 300;
+ self.attackDamage = 15; // Base damage for regular potion, base heal for heal potion
+ self.moveSpeed = 0.9;
+ self.health = 65; // Fragile
+ self.maxHealth = 65;
+ self.attackCooldown = 150; // Slower attack rate
+ self.special = "brews random potions (damage, heal, buff)";
+ self.isLiving = true;
+ self.attackType = 'ranged';
+ self.flying = false;
+ break;
case 'gatlingGunner':
self.attackRange = Infinity; // Infinite range
self.attackDamage = 8; // Increased damage per bullet
self.moveSpeed = 0; // Stands still
@@ -1074,26 +1114,9 @@
// Movement specific properties
self.targetX = Math.random() * battlefield.width;
self.targetY = Math.random() * (battlefield.height * 0.5); // Fly in the upper half
break;
- case 'alchemist':
- self.attackRange = 250;
- self.attackDamage = 5; // Base damage is low, relies on potions
- self.moveSpeed = 1.0;
- self.health = 60; // Fragile
- self.maxHealth = 60;
- self.attackCooldown = 120; // 2 seconds
- self.baseAttackCooldown = 120;
- self.special = "brews random potions";
- self.isLiving = true;
- self.attackType = 'special'; // Use a special type to handle unique logic
- self.flying = false;
- break;
}
- // Store base cooldown if not already set (for units defined before buff logic)
- if (self.baseAttackCooldown === 0) {
- self.baseAttackCooldown = self.attackCooldown;
- }
// Create unit appearance
var unitShape = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
@@ -1122,8 +1145,32 @@
self.updateHealthBar = function () {
var healthPercent = self.health / self.maxHealth;
self.healthBarFill.scaleX = healthPercent;
};
+ self.applyBuff = function (buffType, source) {
+ self.clearBuff(); // Clear any existing buffs first
+ self.buffType = buffType;
+ self.buffSource = source;
+ if (buffType === 'acidicDip') {
+ self.buffAttackCount = 5;
+ self.buffDamageMultiplier = 1.3; // 30% more damage
+ self.buffAttackSpeedMultiplier = 0.7; // 30% faster attacks (70% of original cooldown)
+ self.buffNonLivingBonus = 10; // +10 damage vs non-living
+ console.log(self.type + " received Acidic Dip buff!");
+ }
+ // Add other buff types here if needed
+ };
+ self.clearBuff = function () {
+ if (self.buffType) {
+ console.log(self.type + " buff " + self.buffType + " expired.");
+ }
+ self.buffType = null;
+ self.buffAttackCount = 0;
+ self.buffSource = null;
+ self.buffDamageMultiplier = 1.0;
+ self.buffAttackSpeedMultiplier = 1.0;
+ self.buffNonLivingBonus = 0;
+ };
self.takeDamage = function (amount, attacker) {
// General flying defense: Flying units are immune to non-flying melee attackers
if (self.flying && attacker && !attacker.flying && attacker.attackType === 'melee') {
return; // Immune to ground melee
@@ -1224,8 +1271,64 @@
}
}
return closestEnemy;
};
+ self.findNearestAlly = function () {
+ var includeSelf = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ var needsBuff = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var closestDist = Infinity;
+ var closestAlly = null;
+ for (var i = 0; i < units.length; i++) {
+ var unit = units[i];
+ if (unit.active && unit.team === self.team && (includeSelf || unit !== self)) {
+ // Optionally check if the ally doesn't already have a buff
+ if (needsBuff && unit.buffType !== null) {
+ continue;
+ }
+ var dist = getDistance(self.x, self.y, unit.x, unit.y);
+ if (dist < closestDist) {
+ closestDist = dist;
+ closestAlly = unit;
+ }
+ }
+ }
+ return closestAlly;
+ };
+ self.findNearestInjuredAlly = function () {
+ var closestDist = Infinity;
+ var mostInjuredAlly = null;
+ var lowestHealthRatio = 1.0; // Start with full health ratio
+ for (var i = 0; i < units.length; i++) {
+ var unit = units[i];
+ if (unit.active && unit.team === self.team && unit !== self && unit.health < unit.maxHealth) {
+ var currentHealthRatio = unit.health / unit.maxHealth;
+ var dist = getDistance(self.x, self.y, unit.x, unit.y);
+ // Prioritize closer allies if health ratios are similar, otherwise prioritize most injured
+ // Or maybe just find the closest injured ally within range? Let's try that first.
+ if (dist < closestDist) {
+ closestDist = dist;
+ mostInjuredAlly = unit;
+ }
+ // Alternative: Find the *most* injured ally regardless of distance (within reason?)
+ /*
+ if (currentHealthRatio < lowestHealthRatio) {
+ lowestHealthRatio = currentHealthRatio;
+ mostInjuredAlly = unit;
+ closestDist = dist; // Keep track of distance for tie-breaking maybe
+ } else if (currentHealthRatio === lowestHealthRatio && dist < closestDist) {
+ // Tie-breaker: choose the closer one if equally injured
+ mostInjuredAlly = unit;
+ closestDist = dist;
+ }
+ */
+ }
+ }
+ // Ensure the target is within attack range if we care about that here
+ // if (mostInjuredAlly && getDistance(self.x, self.y, mostInjuredAlly.x, mostInjuredAlly.y) > self.attackRange) {
+ // return null; // Target out of range
+ // }
+ return mostInjuredAlly;
+ };
self.moveTowardsTarget = function () {
if (!self.target || !self.target.active) {
if (self.type === 'spearman' && Math.random() < 0.3) {
// 30% chance to throw spear
@@ -1324,94 +1427,116 @@
self.die();
return;
}
}
- if (!self.target || !self.target.active) {
- self.target = self.findNearestEnemy();
- if (!self.target) {
- return;
- }
+ if (self.buffType && self.buffAttackCount <= 0) {
+ self.clearBuff();
}
- // Brachiosaur is a pacifist and doesn't attack
- if (self.type === 'brachiosaur') {
- return;
- }
- // Check for Alchemist special attack first
+ // Check for Alchemist first
if (self.type === 'alchemist') {
- var potionType = Math.floor(Math.random() * 3); // 0: Regular, 1: Heal, 2: Buff
- if (potionType === 0) {
- // Regular Potion (Attack Enemy)
- var enemyTarget = self.findNearestEnemy();
- if (enemyTarget && getDistance(self.x, self.y, enemyTarget.x, enemyTarget.y) <= self.attackRange) {
- battlefield.createProjectile(self, enemyTarget, 'regularPotion');
- LK.getSound('spell').play(); // Sound for brewing/throwing
- self.attackCooldown = self.baseAttackCooldown;
+ // Decide which potion to brew (randomly for now)
+ var potionChoice = Math.floor(Math.random() * 3); // 0, 1, or 2
+ var currentTarget = null;
+ var potionType = '';
+ if (potionChoice === 0) {
+ // Regular Potion (damage enemy)
+ currentTarget = self.findNearestEnemy();
+ potionType = 'regularPotion';
+ if (currentTarget && getDistance(self.x, self.y, currentTarget.x, currentTarget.y) <= self.attackRange) {
+ battlefield.createProjectile(self, currentTarget, potionType);
+ LK.getSound('attack').play(); // Generic attack sound for throwing
} else {
- // No valid target in range, maybe wait? Or reset cooldown partially?
- // For now, just reset full cooldown to prevent spamming checks.
- self.attackCooldown = 10; // Short cooldown if no target found
+ // No valid enemy target in range, maybe try another potion next time?
+ // Or just reset cooldown without attacking
+ self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier);
+ return;
}
- } else if (potionType === 1) {
- // Heal Potion (Heal Ally)
- var allyToHeal = null;
- var lowestHealthPercent = Infinity;
- for (var i = 0; i < units.length; i++) {
- var unit = units[i];
- if (unit.active && unit.team === self.team && unit !== self && unit.health < unit.maxHealth) {
- var distToAlly = getDistance(self.x, self.y, unit.x, unit.y);
- var healthPercent = unit.health / unit.maxHealth;
- if (distToAlly <= self.attackRange && healthPercent < lowestHealthPercent) {
- lowestHealthPercent = healthPercent;
- allyToHeal = unit;
- }
- }
- }
- if (allyToHeal) {
- battlefield.createProjectile(self, allyToHeal, 'healPotion');
- LK.getSound('spell').play();
- self.attackCooldown = self.baseAttackCooldown;
+ } else if (potionChoice === 1) {
+ // Heal Potion (heal ally)
+ currentTarget = self.findNearestInjuredAlly();
+ potionType = 'healPotion';
+ // Target the ally's current position for the heal AoE center
+ if (currentTarget && getDistance(self.x, self.y, currentTarget.x, currentTarget.y) <= self.attackRange) {
+ // Create a temporary target object for the projectile system
+ var healTargetLocation = {
+ x: currentTarget.x,
+ y: currentTarget.y,
+ active: true,
+ team: self.team
+ }; // Needs team?
+ battlefield.createProjectile(self, healTargetLocation, potionType);
+ LK.getSound('attack').play(); // Generic attack sound
} else {
- self.attackCooldown = 10;
+ // No injured ally in range, reset cooldown
+ self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier);
+ return;
}
} else {
- // Acidic Mixture Dip (Buff Ally)
- var allyToBuff = null;
- var closestAllyDist = Infinity;
- for (var i = 0; i < units.length; i++) {
- var unit = units[i];
- // Buff any active ally (not self) that doesn't already have this buff
- if (unit.active && unit.team === self.team && unit !== self && unit.buff !== 'acidicMixtureDip') {
- var distToAlly = getDistance(self.x, self.y, unit.x, unit.y);
- if (distToAlly <= self.attackRange && distToAlly < closestAllyDist) {
- closestAllyDist = distToAlly;
- allyToBuff = unit;
- }
- }
- }
- if (allyToBuff) {
- allyToBuff.buff = 'acidicMixtureDip';
- allyToBuff.remainingBuffAttacks = 5;
- allyToBuff.baseAttackCooldown = allyToBuff.attackCooldown; // Store current cooldown before buffing
- allyToBuff.attackCooldown = Math.max(1, Math.floor(allyToBuff.baseAttackCooldown / 2)); // Halve attack cooldown (ensure minimum 1)
- LK.effects.flashObject(allyToBuff, 0xADFF2F, 500); // Flash green yellow
- LK.getSound('Holy').play(); // Reuse sound?
- self.attackCooldown = self.baseAttackCooldown;
+ // Acidic Mixture Dip (buff ally)
+ currentTarget = self.findNearestAlly(false, true); // Find nearest ally excluding self that needs a buff
+ potionType = 'acidicMixturePotion';
+ if (currentTarget && getDistance(self.x, self.y, currentTarget.x, currentTarget.y) <= self.attackRange) {
+ battlefield.createProjectile(self, currentTarget, potionType);
+ LK.getSound('attack').play(); // Generic attack sound
} else {
- self.attackCooldown = 10;
+ // No ally to buff in range, reset cooldown
+ self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier);
+ return;
}
}
- return; // Alchemist attack handled, exit function
+ // Reset Alchemist's cooldown (applies buff speed multiplier if alchemist is buffed)
+ // Need to store max cooldown somewhere, let's assume it's the initial value from constructor
+ if (!self.maxAttackCooldown) {
+ self.maxAttackCooldown = self.attackCooldown;
+ } // Store on first attack if not set
+ self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier);
+ return; // Alchemist logic finished
}
- // --- Original Attack Logic below, modified for buffs ---
+ // Existing target check for other units
+ if (!self.target || !self.target.active) {
+ // Handle non-Alchemist targeting (e.g., healers finding allies)
+ if (self.type === 'appleThrower' || self.type === 'priest') {
+ self.target = self.findNearestInjuredAlly();
+ } else {
+ self.target = self.findNearestEnemy();
+ }
+ if (!self.target) {
+ return;
+ }
+ }
+ // Brachiosaur is a pacifist and doesn't attack
+ if (self.type === 'brachiosaur') {
+ return;
+ }
if (self.attackCooldown === 0) {
// Ensure attack is only called once per cooldown cycle
- // Cooldown reset is now handled AFTER the attack action, considering buffs
- self.attack(); //{aB} // Call internal attack logic, which now includes cooldown reset
+ self.attackCooldown = self.type === 'wobbler' ? 40 : self.type === 'farmer' ? 60 : self.type === 'archer' ? 80 : self.type === 'shield' ? 50 : self.type === 'sword' ? 45 : self.type === 'jeep' ? 20 : self.type === 'raptor' ? 30 : self.type === 't-rex' ? 90 :
+ // Set Jeep cooldown to 0.333 seconds (20 frames)
+ 100;
+ self.attack();
}
var dist = self.target ? getDistance(self.x, self.y, self.target.x, self.target.y) : Infinity;
if (self.target && dist <= self.attackRange) {
+ // Consume buff charge if attacking
+ if (self.buffType && self.buffAttackCount > 0 && self.target.team !== self.team) {
+ // Only consume on damaging attacks
+ self.buffAttackCount--;
+ console.log(self.type + " buff attacks remaining: " + self.buffAttackCount);
+ if (self.buffAttackCount <= 0) {
+ // Schedule buff removal after this attack resolves
+ LK.setTimeout(function () {
+ self.clearBuff();
+ }, 10); // Short delay
+ }
+ }
// Calculate damage based on unit type
var damage = self.attackDamage;
+ // Apply buff damage multiplier
+ damage *= self.buffDamageMultiplier;
+ // Apply buff non-living bonus
+ if (self.buffNonLivingBonus > 0 && self.target && !self.target.isLiving) {
+ damage += self.buffNonLivingBonus;
+ }
// Raptor pack bonus: increase damage when near other raptors
if (self.type === 'raptor') {
var nearbyRaptors = 0;
for (var i = 0; i < units.length; i++) {
@@ -1436,8 +1561,14 @@
// Reset attack cooldown
self.attackCooldown = self.type === 'wobbler' ? 40 : self.type === 'farmer' ? 60 : self.type === 'archer' ? 80 : self.type === 'shield' ? 50 : self.type === 'sword' ? 45 : self.type === 'jeep' ? 20 : self.type === 'raptor' ? 30 : self.type === 't-rex' ? 90 :
// Set Jeep cooldown to 0.333 seconds (20 frames)
100;
+ // Store max cooldown if not already set (needed for buff calculation)
+ if (!self.maxAttackCooldown) {
+ self.maxAttackCooldown = self.attackCooldown;
+ }
+ // Apply buff speed multiplier
+ self.attackCooldown = Math.floor(self.maxAttackCooldown * self.buffAttackSpeedMultiplier);
// Handle special attacks
if (self.type === 'appleThrower') {
// Apple thrower finds injured ally instead of enemy
var lowestHealth = Infinity;
@@ -1636,38 +1767,9 @@
if (self.special === "firstStrike" && !self.hasAttacked) {
damage *= 1.5; // 50% bonus on first hit
self.hasAttacked = true;
}
- // Check for Acidic Mixture Dip buff BEFORE dealing damage/healing
- var isBuffed = self.buff === 'acidicMixtureDip' && self.remainingBuffAttacks > 0;
- var buffDamageBonus = 0;
- var buffVsNonLivingBonus = 0;
- if (isBuffed) {
- buffDamageBonus = 10; // Additive damage bonus
- if (self.target && !self.target.isLiving) {
- buffVsNonLivingBonus = 15; // Extra damage vs non-living
- }
- damage += buffDamageBonus + buffVsNonLivingBonus;
- }
self.target.takeDamage(damage, self);
- // Decrement buff counter AFTER attack is successful
- if (isBuffed) {
- self.remainingBuffAttacks--;
- if (self.remainingBuffAttacks <= 0) {
- self.buff = null;
- // Restore normal attack speed - cooldown will be set to base next cycle
- // No immediate change needed here, the cooldown reset below handles it.
- LK.effects.flashObject(self, 0xff0000, 300); // Flash red briefly when buff expires
- }
- }
- // Reset attack cooldown AFTER attack action, considering buff state
- if (isBuffed) {
- // Keep buffed (faster) cooldown if buff is still active
- self.attackCooldown = Math.max(1, Math.floor(self.baseAttackCooldown / 2));
- } else {
- // Reset to normal base cooldown if not buffed (or buff just expired)
- self.attackCooldown = self.baseAttackCooldown;
- }
} else if (self.special === "plasma rifle") {
// Robot Blaster attack logic
battlefield.createProjectile(self, self.target, 'bullet');
LK.getSound('Blaster').play();
@@ -1960,13 +2062,13 @@
/****
* Game Code
****/
-// Heal pool effect
-// Acid pool effect
-// Heal potion projectile
-// Acid potion projectile
-// New Alchemist shape
+// Light green, semi-transparent
+// Lime green, semi-transparent
+// Yellow
+// Bright green
+// Dark red/brown
// Define factions and their respective units
var endlessModeInstructions = new Text2("Endless Mode Instructions:\n- Place units on the battlefield.\n- Survive waves of enemies.\n- Unlock new units as you progress.\n- Win by unlocking all units.", {
size: 30,
fill: 0xFFFFFF
@@ -2130,18 +2232,17 @@
farm: ['wobbler', 'farmer', 'appleThrower', 'scarecrow', 'chicken'],
medieval: ['sword', 'archer', 'crossbowMan', 'king', 'shield', 'cannon', 'queen', 'catapult', 'priest'],
rome: ['spearman', 'retarius', 'romanJavelinThrower', 'romanLegionnaire', 'ballista'],
magic: ['mirrorShield', 'wizard', 'necromancer', 'skeleton', 'witch', 'apprentice', 'alchemist'],
- // Added Alchemist
vikings: ['berserker', 'hammer', 'axeThrower', 'jarl', 'vikingBearRider'],
// Added Viking Bear Rider
// Removed Valkyrie
modern: ['sniper', 'warMachine', 'rifleman', 'mortar', 'gatlingGunner', 'jeep', 'plane'],
pirates: ['swashbuckler', 'pirateCrew', 'pirateCaptain', 'bombThrower'],
dinosaurs: ['raptor', 'brachiosaur', 't-rex', 't-rexJr'],
// Added dinosaur units
robots: ['robotBlaster', 'explodingSpiderBot'] // Added Robot Blaster and Exploding Spider Bot to robots faction
-}, "magic", ['mirrorShield', 'wizard', 'necromancer', 'skeleton', 'witch', 'apprentice', 'alchemist']), "vikings", ['berserker', 'hammer', 'axeThrower', 'jarl', 'vikingBearRider']), "modern", ['sniper', 'warMachine', 'rifleman', 'mortar', 'gatlingGunner', 'jeep', 'plane'], "farm", ['wobbler', 'farmer', 'appleThrower', 'scarecrow', 'chicken']); // Added Alchemist to magic
+}, "rome", ['spearman', 'retarius', 'romanJavelinThrower', 'romanLegionnaire', 'ballista']), "vikings", ['berserker', 'hammer', 'axeThrower', 'jarl', 'vikingBearRider']), "modern", ['sniper', 'warMachine', 'rifleman', 'mortar', 'gatlingGunner', 'jeep', 'plane'], "farm", ['wobbler', 'farmer', 'appleThrower', 'scarecrow', 'chicken']); // Removed Valkyrie, Added Viking Bear Rider
// Game state variables
var units = [];
var draggingUnit = null;
var battlefield = null;
@@ -2227,15 +2328,8 @@
battlefield.projectiles[i].destroy();
}
}
battlefield.projectiles = [];
- // Clear pools
- for (var k = battlefield.pools.length - 1; k >= 0; k--) {
- if (battlefield.pools[k].parent) {
- battlefield.pools[k].destroy();
- }
- }
- battlefield.pools = [];
// Reset battlefield state
battlefield.initialized = false;
// Show start button, endless mode button, and instructions again
if (startButton) {
victory
Sound effect
battleMusic
Music
death
Sound effect
arrow
Sound effect
spell
Sound effect
attack
Sound effect
Explosion
Sound effect
Destroyed
Sound effect
Kiss
Sound effect
Gnehehe
Sound effect
Defend
Sound effect
Girldeath
Sound effect
Shoot
Sound effect
RAWR
Sound effect
Malfunction
Sound effect
Blaster
Sound effect
Holy
Sound effect