/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AreaDamage = Container.expand(function (centerX, centerY, radius, damage, excludeEnemy) {
var self = Container.call(this);
self.x = centerX;
self.y = centerY;
self.radius = radius;
self.damage = damage;
self.excludeEnemy = excludeEnemy;
self.lifetime = 5; // Frames to exist
// Create visual effect
var assetName = radius > 100 ? 'largeAreaDamage' : 'smallAreaDamage';
var visualEffect = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
visualEffect.alpha = 0.6;
// Animate the visual effect
tween(visualEffect, {
alpha: 0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 2000,
easing: tween.easeOut
});
self.update = function () {
// Deal damage to all enemies in radius
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy === self.excludeEnemy) continue; // Skip the original target
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
enemy.takeDamage(self.damage);
// If enemy health becomes negative, destroy immediately
if (enemy.health <= 0) {
enemy.die();
enemy.destroy();
for (var j = enemies.length - 1; j >= 0; j--) {
if (enemies[j] === enemy) {
enemies.splice(j, 1);
break;
}
}
}
}
}
self.lifetime--;
if (self.lifetime <= 0) {
self.shouldDestroy = true;
}
};
return self;
});
var Bullet = Container.expand(function (startX, startY, damage, target, bulletType) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.speed = 8;
self.damage = damage || 1;
self.target = target;
self.bulletType = bulletType || 'bullet';
var bulletGraphics = self.attachAsset(self.bulletType, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (self.target && self.target.parent) {
// Calculate direction to target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Move towards target
var directionX = dx / distance;
var directionY = dy / distance;
self.x += directionX * self.speed;
self.y += directionY * self.speed;
// Store direction for straight line movement
self.directionX = directionX;
self.directionY = directionY;
} else {
// Close enough to target
if (self.target.takeDamage) {
self.target.takeDamage(self.damage);
}
self.shouldDestroy = true;
}
} else {
// Target is gone, continue in straight line
if (self.directionX !== undefined && self.directionY !== undefined) {
self.x += self.directionX * self.speed;
self.y += self.directionY * self.speed;
} else {
// No direction set, destroy bullet
self.shouldDestroy = true;
}
}
};
return self;
});
var Defender = Container.expand(function (type, level) {
var self = Container.call(this);
self.type = type || 'green';
self.level = level || 1;
self.shootTimer = 0;
self.shootInterval = 60; // 1 second at 60fps
self.damage = 1;
var assetNames = {
'green': 'archer',
'yellow': 'spearFighter',
'red': 'musketeer',
'orange': 'cannon',
'blue': 'skyMage',
'black': 'darkMage'
};
var defenderGraphics = self.attachAsset(assetNames[self.type], {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Check if linked die just finished rolling
if (self.linkedDie && self.linkedDie.justFinishedRolling) {
self.shoot();
}
};
self.shoot = function () {
var die = self.linkedDie;
if (die && enemies.length > 0 && die.justFinishedRolling) {
// Find nearest enemy
var nearestEnemy = null;
var shortestDistance = Infinity;
for (var e = 0; e < enemies.length; e++) {
var enemy = enemies[e];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < shortestDistance) {
shortestDistance = distance;
nearestEnemy = enemy;
}
}
if (nearestEnemy) {
var shots = die.getRollValue();
var bulletTypes = {
'green': 'arrow',
'yellow': 'spear',
'red': 'musketBall',
'orange': 'cannonBall',
'blue': 'blueMagic',
'black': 'darkMagic'
};
var bulletType = bulletTypes[self.type] || 'bullet';
// Set damage based on defender type
var bulletDamage = self.damage;
if (self.type === 'red') {
bulletDamage = 2; // Musketeer deals 2 damage
} else if (self.type === 'blue') {
bulletDamage = 2; // Sky mage deals 2 damage
}
// Fire bullets in sequence with 0.3 second intervals
for (var i = 0; i < shots; i++) {
LK.setTimeout(function (shotIndex) {
return function () {
// Find target again (in case original target was destroyed)
var currentTarget = nearestEnemy;
if (!currentTarget || !currentTarget.parent) {
// Find new nearest enemy
for (var e = 0; e < enemies.length; e++) {
var enemy = enemies[e];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (!currentTarget || distance < Math.sqrt((currentTarget.x - self.x) * (currentTarget.x - self.x) + (currentTarget.y - self.y) * (currentTarget.y - self.y))) {
currentTarget = enemy;
}
}
}
if (currentTarget && currentTarget.parent) {
var bullet = new Bullet(self.x, self.y - 40, bulletDamage, currentTarget, bulletType);
bullets.push(bullet);
game.addChild(bullet);
LK.getSound('shoot').play();
}
};
}(i), i * 300); // 300ms = 0.3 seconds
}
die.justFinishedRolling = false;
}
}
};
return self;
});
var Die = Container.expand(function (type, level) {
var self = Container.call(this);
self.type = type || 'green';
self.level = level || 1;
self.rollTimer = 0;
self.rollInterval = 180; // 3 seconds at 60fps
self.lastRoll = 1;
self.isDragging = false;
self.justFinishedRolling = false;
self.originalPosition = {
x: 0,
y: 0
};
var assetNames = {
'green': 'greenDie',
'yellow': 'yellowDie',
'red': 'redDie',
'orange': 'orangeDie',
'blue': 'blueDie',
'black': 'blackDie'
};
var dieGraphics = self.attachAsset(assetNames[self.type], {
anchorX: 0.5,
anchorY: 0.5
});
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
var levelText = new Text2(self.level.toString(), {
size: 40,
fill: textColor
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 0;
levelText.y = -20;
self.addChild(levelText);
var rollText = new Text2(self.lastRoll.toString(), {
size: 60,
fill: textColor
});
rollText.anchor.set(0.5, 0.5);
rollText.x = 0;
rollText.y = 20;
self.addChild(rollText);
self.down = function (x, y, obj) {
self.isDragging = true;
self.originalPosition.x = self.x;
self.originalPosition.y = self.y;
draggedDie = self;
};
self.update = function () {
self.rollTimer++;
// Blue dice rotate every 2 seconds (120 ticks), others every 3 seconds (180 ticks)
var currentInterval = self.rollInterval;
if (self.type === 'blue') {
currentInterval = 120; // 2 seconds for blue dice
}
if (self.rollTimer >= currentInterval) {
self.rollTimer = 0;
// Unfair dice roll - higher probability for lower values
var rollWeights = [35, 25, 20, 12, 6, 2]; // Weights for 1,2,3,4,5,6
var totalWeight = 0;
for (var w = 0; w < rollWeights.length; w++) {
totalWeight += rollWeights[w];
}
var random = Math.random() * totalWeight;
var currentWeight = 0;
self.lastRoll = 1; // Default to 1
for (var w = 0; w < rollWeights.length; w++) {
currentWeight += rollWeights[w];
if (random <= currentWeight) {
self.lastRoll = w + 1;
break;
}
}
var displayValue = self.lastRoll + (self.level - 1) * 2;
rollText.setText(displayValue.toString());
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
rollText.fill = textColor;
self.justFinishedRolling = true;
// Add rolling animation
tween(dieGraphics, {
rotation: Math.PI * 2
}, {
duration: 500,
easing: tween.easeOut
});
}
};
self.getRollValue = function () {
return self.lastRoll + (self.level - 1) * 2;
};
self.updateLevel = function (newLevel) {
self.level = newLevel;
levelText.setText(self.level.toString());
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
levelText.fill = textColor;
rollText.fill = textColor;
};
return self;
});
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'troll';
// Set stats based on enemy type
var enemyStats = {
'troll': {
health: 5,
speed: 1,
goldValue: Math.floor(Math.random() * 8) + 5,
asset: 'troll',
behavior: 'melee'
},
'skeleton': {
health: 10,
speed: 1.5,
goldValue: Math.floor(Math.random() * 11) + 8,
asset: 'skeleton',
behavior: 'melee'
},
'knight': {
health: 1,
speed: 3,
goldValue: Math.floor(Math.random() * 5) + 3,
asset: 'knight',
behavior: 'melee'
},
'witch': {
health: 30,
speed: 0.5,
goldValue: Math.floor(Math.random() * 15) + 12,
asset: 'witch',
behavior: 'summoner'
},
'golem': {
health: 100,
speed: 0.8,
goldValue: Math.floor(Math.random() * 23) + 23,
asset: 'golem',
behavior: 'melee'
},
'demon': {
health: 75,
speed: 1.2,
goldValue: Math.floor(Math.random() * 18) + 15,
asset: 'demon',
behavior: 'ranged'
}
};
var stats = enemyStats[self.type] || enemyStats['troll'];
self.health = stats.health;
self.maxHealth = stats.health;
self.speed = stats.speed;
self.goldValue = stats.goldValue;
self.behavior = stats.behavior;
self.summonTimer = 0;
self.attackTimer = 0;
self.hasAttackedWall = false;
self.skeletonCount = 0; // Track how many skeletons this witch has created
self.spawnedSkeletons = []; // Track spawned skeletons for cleanup
var enemyGraphics = self.attachAsset(stats.asset, {
anchorX: 0.5,
anchorY: 0.5
});
var healthBar = new Text2(self.health.toString(), {
size: 30,
fill: 0xFF0000
});
healthBar.anchor.set(0.5, 0.5);
healthBar.x = 0;
healthBar.y = -80;
self.addChild(healthBar);
self.update = function () {
if (self.behavior === 'ranged' && self.y >= 1500) {
// Demon ranged attack behavior - stop and shoot
self.attackTimer++;
if (self.attackTimer >= 120) {
// Every 2 seconds
self.attackTimer = 0;
self.rangedAttack();
}
} else if (self.behavior === 'summoner') {
// Witch behavior - move slowly (no longer summons skeletons)
if (self.y < 1770) {
self.y += self.speed;
}
} else if (self.y >= 1770 && !self.hasAttackedWall) {
// Reached wall - deal damage
self.hasAttackedWall = true;
self.attackWall();
} else if (self.y < 1770) {
// Normal movement
self.y += self.speed;
}
};
self.rangedAttack = function () {
if (wallHealth > 0) {
var projectile = new RangedProjectile(self.x, self.y, 1770);
rangedProjectiles.push(projectile);
game.addChild(projectile);
}
};
self.summonSkeleton = function () {
// Check if we already have 3 skeletons
if (self.skeletonCount >= 3) {
return; // Don't spawn more skeletons
}
var skeleton = new Enemy('skeleton');
skeleton.x = self.x + (Math.random() - 0.5) * 100;
skeleton.y = self.y + 50;
skeleton.createdBy = self; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
self.skeletonCount++;
self.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
};
self.attackWall = function () {
wallHealth -= 1;
wallHealthText.setText('Wall HP: ' + wallHealth);
LK.getSound('wallHit').play();
LK.effects.flashObject(wall, 0xFF0000, 300);
if (wallHealth <= 0) {
// Game over
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
};
self.takeDamage = function (damage) {
self.health -= damage;
healthBar.setText(self.health.toString());
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
gold += self.goldValue;
goldText.setText('Gold: ' + gold);
LK.getSound('enemyDie').play();
LK.effects.flashObject(self, 0xFF0000, 500);
// If this is a witch, clean up references to spawned skeletons and decrement witch count
if (self.type === 'witch') {
witchCount--;
for (var i = 0; i < self.spawnedSkeletons.length; i++) {
var skeleton = self.spawnedSkeletons[i];
if (skeleton && skeleton.createdBy === self) {
skeleton.createdBy = null;
}
}
}
// Decrement golem count if this is a golem
if (self.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
};
return self;
});
var RangedProjectile = Container.expand(function (startX, startY, targetY) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.targetY = targetY;
self.speed = 4;
var projectileGraphics = self.attachAsset('rangedAttack', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.y += self.speed;
if (self.y >= self.targetY) {
// Hit the wall
wallHealth -= 1; // Each enemy deals 1 damage to wall
wallHealthText.setText('Wall HP: ' + wallHealth);
LK.getSound('wallHit').play();
LK.effects.flashObject(wall, 0xFF0000, 300);
self.shouldDestroy = true;
if (wallHealth <= 0) {
// Game over
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F5233
});
/****
* Game Code
****/
// Add fantasy world background image
var fantasyBackground = game.addChild(LK.getAsset('fantasyWorldBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Game variables
var gold = 10;
var enemies = [];
var bullets = [];
var dice = [];
var defenders = [];
var wallSlots = [];
var diceSlots = [];
var draggedDie = null;
var trashArea = null;
var enemySpawnTimer = 0;
var enemySpawnInterval = 300; // 5 seconds initially
var gameTimer = 0; // Track game time in ticks
var dicesPurchased = 0; // Track number of dice purchased for pricing
var wallHealth = 10;
var rangedProjectiles = [];
var witchCount = 0; // Track number of witches currently on field
var golemCount = 0; // Track number of golems currently on field
var totalEnemyCount = 0; // Track total number of enemies on field
// UI elements
var goldText = new Text2('Gold: 10', {
size: 60,
fill: 0xFFFF00
});
goldText.anchor.set(0, 0);
goldText.x = 150;
goldText.y = 50;
LK.gui.topLeft.addChild(goldText);
// Wall health display
var wallHealthText = new Text2('Wall HP: 10', {
size: 50,
fill: 0xFF0000
});
wallHealthText.anchor.set(0, 0);
wallHealthText.x = 150;
wallHealthText.y = 120;
LK.gui.topLeft.addChild(wallHealthText);
// Stage indicator display
var stageText = new Text2('Stage: 1', {
size: 50,
fill: 0x00FF00
});
stageText.anchor.set(1, 0);
stageText.x = -50;
stageText.y = 50;
LK.gui.topRight.addChild(stageText);
// Create wall
var wall = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 1,
x: 1024,
y: 1900
}));
// Create wall slots (9 defensive positions)
for (var i = 0; i < 9; i++) {
var slot = game.addChild(LK.getAsset('wallSlot', {
anchorX: 0.5,
anchorY: 0.5,
x: 200 + i * 200,
y: 1900 - 75
}));
wallSlots.push(slot);
}
// Create dice grid (3x3)
var diceGridStartX = 824; // Center on screen (1024 - 200)
var diceGridStartY = 2100;
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var slot = game.addChild(LK.getAsset('diceSlot', {
anchorX: 0.5,
anchorY: 0.5,
x: diceGridStartX + col * 200,
y: diceGridStartY + row * 200
}));
slot.isEmpty = true;
slot.gridIndex = row * 3 + col;
diceSlots.push(slot);
}
}
// Create trash area to the left of dice grid
trashArea = game.addChild(LK.getAsset('trashArea', {
anchorX: 0.5,
anchorY: 0.5,
x: 524,
// Left of dice grid
y: 2200
}));
// Buy button
var buyButton = game.addChild(LK.getAsset('buyButton', {
anchorX: 0.5,
anchorY: 0.5,
x: 1600,
y: 2200
}));
// Price display text
var priceText = new Text2('Price: 10', {
size: 40,
fill: 0xFFFFFF
});
priceText.anchor.set(0.5, 0);
priceText.x = 1600;
priceText.y = 2400;
game.addChild(priceText);
buyButton.down = function (x, y, obj) {
var currentPrice = 10 + dicesPurchased * 5;
if (gold >= currentPrice) {
buyDie();
}
};
function buyDie() {
var emptySlot = null;
for (var i = 0; i < diceSlots.length; i++) {
if (diceSlots[i].isEmpty) {
emptySlot = diceSlots[i];
break;
}
}
if (emptySlot) {
var currentPrice = 10 + dicesPurchased * 5;
gold -= currentPrice;
dicesPurchased++;
goldText.setText('Gold: ' + gold);
// Update price display for next purchase
var nextPrice = 10 + dicesPurchased * 5;
priceText.setText('Price: ' + nextPrice);
var dieTypes = ['green', 'yellow', 'red', 'orange', 'blue', 'black'];
var weights = [30, 30, 20, 20, 10, 10]; // Common, Common, Rare, Rare, Legendary, Legendary
var randomType = getWeightedRandom(dieTypes, weights);
var newDie = new Die(randomType, 1);
newDie.x = emptySlot.x;
newDie.y = emptySlot.y;
newDie.slotIndex = emptySlot.gridIndex;
dice.push(newDie);
game.addChild(newDie);
emptySlot.isEmpty = false;
emptySlot.occupiedBy = newDie;
// Create corresponding defender
createDefender(newDie);
}
}
function getWeightedRandom(items, weights) {
var totalWeight = 0;
for (var i = 0; i < weights.length; i++) {
totalWeight += weights[i];
}
var random = Math.random() * totalWeight;
var currentWeight = 0;
for (var i = 0; i < items.length; i++) {
currentWeight += weights[i];
if (random <= currentWeight) {
return items[i];
}
}
return items[0];
}
function createDefender(die) {
var defender = new Defender(die.type, die.level);
var wallSlot = wallSlots[die.slotIndex % 9];
defender.x = wallSlot.x;
defender.y = wallSlot.y;
defender.linkedDie = die;
defenders.push(defender);
game.addChild(defender);
}
function spawnEnemy() {
// Check total enemy limit
if (totalEnemyCount >= 20) {
return; // Don't spawn if at maximum enemy count
}
var enemyType = 'troll';
var enemiesToSpawn = 1;
var spawnPattern = 'single';
// Stage 1: Only trolls (0-60 seconds) - slow spawn rate
if (gameTimer <= 3600) {
enemyType = 'troll';
spawnPattern = 'single';
}
// Stage 2: Trolls + skeletons (60-120 seconds) - faster spawn rate
else if (gameTimer <= 7200) {
enemyType = Math.random() < 0.6 ? 'troll' : 'skeleton';
spawnPattern = 'single';
}
// Stage 3: Trolls + knights (120-165 seconds) - trolls get more health
else if (gameTimer <= 9900) {
enemyType = Math.random() < 0.7 ? 'troll' : 'knight';
spawnPattern = 'single';
}
// Stage 4: Knights + witches + trolls (165-210 seconds)
else if (gameTimer <= 12600) {
var rand = Math.random();
if (rand < 0.4) enemyType = 'knight';else if (rand < 0.7 && witchCount < 4) enemyType = 'witch';else enemyType = 'troll';
spawnPattern = 'single';
}
// Stage 5: Golems in front, then knights behind, then witches (210+ seconds)
else {
var rand = Math.random();
if (rand < 0.3 && golemCount < 3) {
enemyType = 'golem';
spawnPattern = 'front';
} else if (rand < 0.7) {
enemyType = 'knight';
spawnPattern = 'behind';
} else if (witchCount < 4) {
enemyType = 'witch';
spawnPattern = 'back';
} else {
enemyType = 'knight';
spawnPattern = 'behind';
}
}
// Spawn enemies based on pattern
if (spawnPattern === 'single') {
var enemy = new Enemy(enemyType);
// Stage 3+: Give trolls and skeletons more health
if (gameTimer > 7200) {
if (enemyType === 'troll') {
enemy.health = 10;
enemy.maxHealth = 10;
enemy.children[1].setText(enemy.health.toString()); // Update health display
} else if (enemyType === 'skeleton') {
enemy.health = 15;
enemy.maxHealth = 15;
enemy.children[1].setText(enemy.health.toString()); // Update health display
}
}
enemy.x = Math.random() * 1688 + 180; // Random X position (avoiding 80px margins)
enemy.y = -100; // Start above screen
if (enemyType === 'witch') {
witchCount++;
// Spawn 2 skeletons with 25 health each in front of witch
for (var s = 0; s < 2; s++) {
var skeleton = new Enemy('skeleton');
skeleton.health = 25;
skeleton.maxHealth = 25;
skeleton.children[1].setText(skeleton.health.toString()); // Update health display
skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch
skeleton.y = enemy.y + 100; // Position skeletons in front of witch
skeleton.createdBy = enemy; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
enemy.skeletonCount++;
enemy.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
}
}
if (enemyType === 'golem') {
golemCount++;
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'front') {
// Spawn golem at front
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -100;
if (enemyType === 'golem') {
golemCount++;
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'behind') {
// Spawn knight behind golems
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -300; // Further back
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'back') {
// Spawn witch at the back
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -500; // Much further back
if (enemyType === 'witch') {
witchCount++;
// Spawn 2 skeletons with 25 health each in front of witch
for (var s = 0; s < 2; s++) {
var skeleton = new Enemy('skeleton');
skeleton.health = 25;
skeleton.maxHealth = 25;
skeleton.children[1].setText(skeleton.health.toString()); // Update health display
skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch
skeleton.y = enemy.y + 100; // Position skeletons in front of witch
skeleton.createdBy = enemy; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
enemy.skeletonCount++;
enemy.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
}
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
}
}
function handleMove(x, y, obj) {
if (draggedDie) {
draggedDie.x = x;
draggedDie.y = y;
}
}
function handleUp(x, y, obj) {
if (draggedDie) {
var merged = false;
var trashed = false;
var moved = false;
// Check if dropped on trash area
if (draggedDie.intersects(trashArea)) {
// Destroy die and give 5 gold
gold += 5;
goldText.setText('Gold: ' + gold);
// Remove die from slot
var slot = diceSlots[draggedDie.slotIndex];
slot.isEmpty = true;
slot.occupiedBy = null;
// Remove corresponding defender
for (var i = defenders.length - 1; i >= 0; i--) {
if (defenders[i].linkedDie === draggedDie) {
defenders[i].destroy();
defenders.splice(i, 1);
break;
}
}
// Remove die
draggedDie.destroy();
dice.splice(dice.indexOf(draggedDie), 1);
trashed = true;
}
if (!trashed) {
// Check if dropped on an empty slot
for (var i = 0; i < diceSlots.length; i++) {
var targetSlot = diceSlots[i];
if (targetSlot.isEmpty && draggedDie.intersects(targetSlot)) {
// Move die to new slot
var oldSlot = diceSlots[draggedDie.slotIndex];
oldSlot.isEmpty = true;
oldSlot.occupiedBy = null;
// Update new slot
targetSlot.isEmpty = false;
targetSlot.occupiedBy = draggedDie;
draggedDie.slotIndex = targetSlot.gridIndex;
draggedDie.x = targetSlot.x;
draggedDie.y = targetSlot.y;
draggedDie.originalPosition.x = targetSlot.x;
draggedDie.originalPosition.y = targetSlot.y;
// Move corresponding defender to new wall position
for (var j = 0; j < defenders.length; j++) {
if (defenders[j].linkedDie === draggedDie) {
var newWallSlot = wallSlots[draggedDie.slotIndex % 9];
defenders[j].x = newWallSlot.x;
defenders[j].y = newWallSlot.y;
break;
}
}
moved = true;
break;
}
}
}
if (!trashed && !moved) {
// Check for merge with other dice
for (var i = 0; i < dice.length; i++) {
var otherDie = dice[i];
if (otherDie !== draggedDie && otherDie.type === draggedDie.type && otherDie.level === draggedDie.level && draggedDie.intersects(otherDie)) {
// Merge dice
mergeDice(draggedDie, otherDie);
merged = true;
break;
}
}
if (!merged) {
// Return to original position
draggedDie.x = draggedDie.originalPosition.x;
draggedDie.y = draggedDie.originalPosition.y;
}
}
draggedDie = null;
}
}
function mergeDice(die1, die2) {
// Remove die1 from game
var slot1 = diceSlots[die1.slotIndex];
slot1.isEmpty = true;
slot1.occupiedBy = null;
// Remove corresponding defender
for (var i = defenders.length - 1; i >= 0; i--) {
if (defenders[i].linkedDie === die1) {
defenders[i].destroy();
defenders.splice(i, 1);
break;
}
}
die1.destroy();
dice.splice(dice.indexOf(die1), 1);
// Upgrade die2
die2.updateLevel(die2.level + 1);
// Update corresponding defender
for (var i = 0; i < defenders.length; i++) {
if (defenders[i].linkedDie === die2) {
defenders[i].level = die2.level;
break;
}
}
LK.getSound('merge').play();
}
game.move = handleMove;
game.up = handleUp;
game.update = function () {
// Update game timer
gameTimer++;
// Update stage indicator
var currentStage = 1;
if (gameTimer > 12600) {
currentStage = 5;
} else if (gameTimer > 9900) {
currentStage = 4;
} else if (gameTimer > 7200) {
currentStage = 3;
} else if (gameTimer > 3600) {
currentStage = 2;
}
stageText.setText('Stage: ' + currentStage);
// Progressive enemy spawning
enemySpawnTimer++;
var currentInterval = enemySpawnInterval;
// Calculate progressive spawn rate increase
var baseInterval = 600; // Start at 10 seconds
var minInterval = 60; // Minimum 1 second spawn rate
var reductionRate = gameTimer * 0.0001; // Gradual reduction over time
currentInterval = Math.max(minInterval, baseInterval - gameTimer * 0.02);
// Stage-based spawn intervals
// Stage 1: Slow spawning (0-60 seconds) - only trolls
if (gameTimer <= 3600) {
currentInterval = 200; // 3.33 seconds (reduced spawn rate)
}
// Stage 2: Moderate spawning (60-120 seconds) - trolls + skeletons
else if (gameTimer <= 7200) {
currentInterval = 105; // 1.75 seconds (reduced from 2.5)
}
// Stage 3: Fast spawning (120-165 seconds) - trolls + knights
else if (gameTimer <= 9900) {
currentInterval = 75; // 1.25 seconds (reduced from 1.5)
}
// Stage 4: Very fast spawning (165-210 seconds) - knights + witches + trolls
else if (gameTimer <= 12600) {
currentInterval = 45; // 0.75 seconds (reduced from 1)
}
// Stage 5: Maximum spawning (210+ seconds) - golems, knights, witches formation
else {
currentInterval = 30; // 0.5 seconds (reduced from 0.75)
}
if (enemySpawnTimer >= currentInterval) {
enemySpawnTimer = 0;
spawnEnemy();
}
// Update bullets and check collisions
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Check if bullet should be destroyed
if (bullet.shouldDestroy || bullet.y < -50 || bullet.y > 2732 || bullet.x < 0 || bullet.x > 2048) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with target or any enemy
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (bullet.intersects(enemy)) {
// Handle area damage for cannon and dark mage
if (bullet.bulletType === 'cannonBall') {
// Small area damage for cannon
var areaDamage = new AreaDamage(enemy.x, enemy.y, 80, bullet.damage, enemy);
areaDamage.lifetime = 120; // 2 seconds at 60fps
game.addChild(areaDamage);
} else if (bullet.bulletType === 'darkMagic') {
// Large area damage for dark mage
var areaDamage = new AreaDamage(enemy.x, enemy.y, 150, bullet.damage, enemy);
areaDamage.lifetime = 120; // 2 seconds at 60fps
game.addChild(areaDamage);
}
enemy.takeDamage(bullet.damage);
var enemyDied = enemy.health <= 0;
if (enemyDied) {
// If this is a skeleton created by a witch, update witch's count
if (enemy.type === 'skeleton' && enemy.createdBy) {
enemy.createdBy.skeletonCount--;
}
// If this is a witch, decrement witch count
if (enemy.type === 'witch') {
witchCount--;
}
// If this is a golem, decrement golem count
if (enemy.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
enemy.destroy();
enemies.splice(j, 1);
}
// Special behavior for blueMagic - continue to next enemy
if (bullet.bulletType === 'blueMagic') {
// Initialize hit count if not set
if (!bullet.hitCount) bullet.hitCount = 0;
bullet.hitCount++;
// Find next enemy to target (if this is the first hit)
if (bullet.hitCount === 1) {
var nextTarget = null;
var shortestDistance = Infinity;
for (var k = 0; k < enemies.length; k++) {
var nextEnemy = enemies[k];
if (nextEnemy !== enemy && nextEnemy.parent) {
var dx = nextEnemy.x - bullet.x;
var dy = nextEnemy.y - bullet.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < shortestDistance) {
shortestDistance = distance;
nextTarget = nextEnemy;
}
}
}
// Set new target if found
if (nextTarget) {
bullet.target = nextTarget;
} else {
// No more targets, destroy bullet
bullet.destroy();
bullets.splice(i, 1);
}
} else {
// Second hit, destroy bullet
bullet.destroy();
bullets.splice(i, 1);
}
} else {
// Regular bullet behavior - only destroy if enemy survived
if (!enemyDied) {
bullet.destroy();
bullets.splice(i, 1);
}
}
hitEnemy = true;
break;
}
}
}
// Update and cleanup area damage objects
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child.shouldDestroy) {
child.destroy();
}
}
// Update ranged projectiles
for (var i = rangedProjectiles.length - 1; i >= 0; i--) {
var projectile = rangedProjectiles[i];
if (projectile.shouldDestroy || projectile.y > 2000) {
projectile.destroy();
rangedProjectiles.splice(i, 1);
}
}
// Check if enemies reached the wall (only for melee enemies)
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.behavior !== 'ranged' && enemy.y >= 1770 && !enemy.hasAttackedWall) {
// Enemy attacks wall - reduce health and update display
enemy.attackWall();
// If this is a skeleton created by a witch, update witch's count
if (enemy.type === 'skeleton' && enemy.createdBy) {
enemy.createdBy.skeletonCount--;
}
// If this is a witch, decrement witch count
if (enemy.type === 'witch') {
witchCount--;
}
// If this is a golem, decrement golem count
if (enemy.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
// Remove enemy after attacking wall
enemy.destroy();
enemies.splice(i, 1);
}
}
};
// Start with one free die
buyDie(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AreaDamage = Container.expand(function (centerX, centerY, radius, damage, excludeEnemy) {
var self = Container.call(this);
self.x = centerX;
self.y = centerY;
self.radius = radius;
self.damage = damage;
self.excludeEnemy = excludeEnemy;
self.lifetime = 5; // Frames to exist
// Create visual effect
var assetName = radius > 100 ? 'largeAreaDamage' : 'smallAreaDamage';
var visualEffect = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
visualEffect.alpha = 0.6;
// Animate the visual effect
tween(visualEffect, {
alpha: 0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 2000,
easing: tween.easeOut
});
self.update = function () {
// Deal damage to all enemies in radius
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy === self.excludeEnemy) continue; // Skip the original target
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
enemy.takeDamage(self.damage);
// If enemy health becomes negative, destroy immediately
if (enemy.health <= 0) {
enemy.die();
enemy.destroy();
for (var j = enemies.length - 1; j >= 0; j--) {
if (enemies[j] === enemy) {
enemies.splice(j, 1);
break;
}
}
}
}
}
self.lifetime--;
if (self.lifetime <= 0) {
self.shouldDestroy = true;
}
};
return self;
});
var Bullet = Container.expand(function (startX, startY, damage, target, bulletType) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.speed = 8;
self.damage = damage || 1;
self.target = target;
self.bulletType = bulletType || 'bullet';
var bulletGraphics = self.attachAsset(self.bulletType, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (self.target && self.target.parent) {
// Calculate direction to target
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Move towards target
var directionX = dx / distance;
var directionY = dy / distance;
self.x += directionX * self.speed;
self.y += directionY * self.speed;
// Store direction for straight line movement
self.directionX = directionX;
self.directionY = directionY;
} else {
// Close enough to target
if (self.target.takeDamage) {
self.target.takeDamage(self.damage);
}
self.shouldDestroy = true;
}
} else {
// Target is gone, continue in straight line
if (self.directionX !== undefined && self.directionY !== undefined) {
self.x += self.directionX * self.speed;
self.y += self.directionY * self.speed;
} else {
// No direction set, destroy bullet
self.shouldDestroy = true;
}
}
};
return self;
});
var Defender = Container.expand(function (type, level) {
var self = Container.call(this);
self.type = type || 'green';
self.level = level || 1;
self.shootTimer = 0;
self.shootInterval = 60; // 1 second at 60fps
self.damage = 1;
var assetNames = {
'green': 'archer',
'yellow': 'spearFighter',
'red': 'musketeer',
'orange': 'cannon',
'blue': 'skyMage',
'black': 'darkMage'
};
var defenderGraphics = self.attachAsset(assetNames[self.type], {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Check if linked die just finished rolling
if (self.linkedDie && self.linkedDie.justFinishedRolling) {
self.shoot();
}
};
self.shoot = function () {
var die = self.linkedDie;
if (die && enemies.length > 0 && die.justFinishedRolling) {
// Find nearest enemy
var nearestEnemy = null;
var shortestDistance = Infinity;
for (var e = 0; e < enemies.length; e++) {
var enemy = enemies[e];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < shortestDistance) {
shortestDistance = distance;
nearestEnemy = enemy;
}
}
if (nearestEnemy) {
var shots = die.getRollValue();
var bulletTypes = {
'green': 'arrow',
'yellow': 'spear',
'red': 'musketBall',
'orange': 'cannonBall',
'blue': 'blueMagic',
'black': 'darkMagic'
};
var bulletType = bulletTypes[self.type] || 'bullet';
// Set damage based on defender type
var bulletDamage = self.damage;
if (self.type === 'red') {
bulletDamage = 2; // Musketeer deals 2 damage
} else if (self.type === 'blue') {
bulletDamage = 2; // Sky mage deals 2 damage
}
// Fire bullets in sequence with 0.3 second intervals
for (var i = 0; i < shots; i++) {
LK.setTimeout(function (shotIndex) {
return function () {
// Find target again (in case original target was destroyed)
var currentTarget = nearestEnemy;
if (!currentTarget || !currentTarget.parent) {
// Find new nearest enemy
for (var e = 0; e < enemies.length; e++) {
var enemy = enemies[e];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (!currentTarget || distance < Math.sqrt((currentTarget.x - self.x) * (currentTarget.x - self.x) + (currentTarget.y - self.y) * (currentTarget.y - self.y))) {
currentTarget = enemy;
}
}
}
if (currentTarget && currentTarget.parent) {
var bullet = new Bullet(self.x, self.y - 40, bulletDamage, currentTarget, bulletType);
bullets.push(bullet);
game.addChild(bullet);
LK.getSound('shoot').play();
}
};
}(i), i * 300); // 300ms = 0.3 seconds
}
die.justFinishedRolling = false;
}
}
};
return self;
});
var Die = Container.expand(function (type, level) {
var self = Container.call(this);
self.type = type || 'green';
self.level = level || 1;
self.rollTimer = 0;
self.rollInterval = 180; // 3 seconds at 60fps
self.lastRoll = 1;
self.isDragging = false;
self.justFinishedRolling = false;
self.originalPosition = {
x: 0,
y: 0
};
var assetNames = {
'green': 'greenDie',
'yellow': 'yellowDie',
'red': 'redDie',
'orange': 'orangeDie',
'blue': 'blueDie',
'black': 'blackDie'
};
var dieGraphics = self.attachAsset(assetNames[self.type], {
anchorX: 0.5,
anchorY: 0.5
});
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
var levelText = new Text2(self.level.toString(), {
size: 40,
fill: textColor
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 0;
levelText.y = -20;
self.addChild(levelText);
var rollText = new Text2(self.lastRoll.toString(), {
size: 60,
fill: textColor
});
rollText.anchor.set(0.5, 0.5);
rollText.x = 0;
rollText.y = 20;
self.addChild(rollText);
self.down = function (x, y, obj) {
self.isDragging = true;
self.originalPosition.x = self.x;
self.originalPosition.y = self.y;
draggedDie = self;
};
self.update = function () {
self.rollTimer++;
// Blue dice rotate every 2 seconds (120 ticks), others every 3 seconds (180 ticks)
var currentInterval = self.rollInterval;
if (self.type === 'blue') {
currentInterval = 120; // 2 seconds for blue dice
}
if (self.rollTimer >= currentInterval) {
self.rollTimer = 0;
// Unfair dice roll - higher probability for lower values
var rollWeights = [35, 25, 20, 12, 6, 2]; // Weights for 1,2,3,4,5,6
var totalWeight = 0;
for (var w = 0; w < rollWeights.length; w++) {
totalWeight += rollWeights[w];
}
var random = Math.random() * totalWeight;
var currentWeight = 0;
self.lastRoll = 1; // Default to 1
for (var w = 0; w < rollWeights.length; w++) {
currentWeight += rollWeights[w];
if (random <= currentWeight) {
self.lastRoll = w + 1;
break;
}
}
var displayValue = self.lastRoll + (self.level - 1) * 2;
rollText.setText(displayValue.toString());
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
rollText.fill = textColor;
self.justFinishedRolling = true;
// Add rolling animation
tween(dieGraphics, {
rotation: Math.PI * 2
}, {
duration: 500,
easing: tween.easeOut
});
}
};
self.getRollValue = function () {
return self.lastRoll + (self.level - 1) * 2;
};
self.updateLevel = function (newLevel) {
self.level = newLevel;
levelText.setText(self.level.toString());
var textColor = self.type === 'yellow' || self.type === 'green' || self.type === 'orange' ? 0x000000 : 0xFFFFFF;
levelText.fill = textColor;
rollText.fill = textColor;
};
return self;
});
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'troll';
// Set stats based on enemy type
var enemyStats = {
'troll': {
health: 5,
speed: 1,
goldValue: Math.floor(Math.random() * 8) + 5,
asset: 'troll',
behavior: 'melee'
},
'skeleton': {
health: 10,
speed: 1.5,
goldValue: Math.floor(Math.random() * 11) + 8,
asset: 'skeleton',
behavior: 'melee'
},
'knight': {
health: 1,
speed: 3,
goldValue: Math.floor(Math.random() * 5) + 3,
asset: 'knight',
behavior: 'melee'
},
'witch': {
health: 30,
speed: 0.5,
goldValue: Math.floor(Math.random() * 15) + 12,
asset: 'witch',
behavior: 'summoner'
},
'golem': {
health: 100,
speed: 0.8,
goldValue: Math.floor(Math.random() * 23) + 23,
asset: 'golem',
behavior: 'melee'
},
'demon': {
health: 75,
speed: 1.2,
goldValue: Math.floor(Math.random() * 18) + 15,
asset: 'demon',
behavior: 'ranged'
}
};
var stats = enemyStats[self.type] || enemyStats['troll'];
self.health = stats.health;
self.maxHealth = stats.health;
self.speed = stats.speed;
self.goldValue = stats.goldValue;
self.behavior = stats.behavior;
self.summonTimer = 0;
self.attackTimer = 0;
self.hasAttackedWall = false;
self.skeletonCount = 0; // Track how many skeletons this witch has created
self.spawnedSkeletons = []; // Track spawned skeletons for cleanup
var enemyGraphics = self.attachAsset(stats.asset, {
anchorX: 0.5,
anchorY: 0.5
});
var healthBar = new Text2(self.health.toString(), {
size: 30,
fill: 0xFF0000
});
healthBar.anchor.set(0.5, 0.5);
healthBar.x = 0;
healthBar.y = -80;
self.addChild(healthBar);
self.update = function () {
if (self.behavior === 'ranged' && self.y >= 1500) {
// Demon ranged attack behavior - stop and shoot
self.attackTimer++;
if (self.attackTimer >= 120) {
// Every 2 seconds
self.attackTimer = 0;
self.rangedAttack();
}
} else if (self.behavior === 'summoner') {
// Witch behavior - move slowly (no longer summons skeletons)
if (self.y < 1770) {
self.y += self.speed;
}
} else if (self.y >= 1770 && !self.hasAttackedWall) {
// Reached wall - deal damage
self.hasAttackedWall = true;
self.attackWall();
} else if (self.y < 1770) {
// Normal movement
self.y += self.speed;
}
};
self.rangedAttack = function () {
if (wallHealth > 0) {
var projectile = new RangedProjectile(self.x, self.y, 1770);
rangedProjectiles.push(projectile);
game.addChild(projectile);
}
};
self.summonSkeleton = function () {
// Check if we already have 3 skeletons
if (self.skeletonCount >= 3) {
return; // Don't spawn more skeletons
}
var skeleton = new Enemy('skeleton');
skeleton.x = self.x + (Math.random() - 0.5) * 100;
skeleton.y = self.y + 50;
skeleton.createdBy = self; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
self.skeletonCount++;
self.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
};
self.attackWall = function () {
wallHealth -= 1;
wallHealthText.setText('Wall HP: ' + wallHealth);
LK.getSound('wallHit').play();
LK.effects.flashObject(wall, 0xFF0000, 300);
if (wallHealth <= 0) {
// Game over
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
};
self.takeDamage = function (damage) {
self.health -= damage;
healthBar.setText(self.health.toString());
if (self.health <= 0) {
self.die();
}
};
self.die = function () {
gold += self.goldValue;
goldText.setText('Gold: ' + gold);
LK.getSound('enemyDie').play();
LK.effects.flashObject(self, 0xFF0000, 500);
// If this is a witch, clean up references to spawned skeletons and decrement witch count
if (self.type === 'witch') {
witchCount--;
for (var i = 0; i < self.spawnedSkeletons.length; i++) {
var skeleton = self.spawnedSkeletons[i];
if (skeleton && skeleton.createdBy === self) {
skeleton.createdBy = null;
}
}
}
// Decrement golem count if this is a golem
if (self.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
};
return self;
});
var RangedProjectile = Container.expand(function (startX, startY, targetY) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.targetY = targetY;
self.speed = 4;
var projectileGraphics = self.attachAsset('rangedAttack', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.y += self.speed;
if (self.y >= self.targetY) {
// Hit the wall
wallHealth -= 1; // Each enemy deals 1 damage to wall
wallHealthText.setText('Wall HP: ' + wallHealth);
LK.getSound('wallHit').play();
LK.effects.flashObject(wall, 0xFF0000, 300);
self.shouldDestroy = true;
if (wallHealth <= 0) {
// Game over
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F5233
});
/****
* Game Code
****/
// Add fantasy world background image
var fantasyBackground = game.addChild(LK.getAsset('fantasyWorldBackground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366
}));
// Game variables
var gold = 10;
var enemies = [];
var bullets = [];
var dice = [];
var defenders = [];
var wallSlots = [];
var diceSlots = [];
var draggedDie = null;
var trashArea = null;
var enemySpawnTimer = 0;
var enemySpawnInterval = 300; // 5 seconds initially
var gameTimer = 0; // Track game time in ticks
var dicesPurchased = 0; // Track number of dice purchased for pricing
var wallHealth = 10;
var rangedProjectiles = [];
var witchCount = 0; // Track number of witches currently on field
var golemCount = 0; // Track number of golems currently on field
var totalEnemyCount = 0; // Track total number of enemies on field
// UI elements
var goldText = new Text2('Gold: 10', {
size: 60,
fill: 0xFFFF00
});
goldText.anchor.set(0, 0);
goldText.x = 150;
goldText.y = 50;
LK.gui.topLeft.addChild(goldText);
// Wall health display
var wallHealthText = new Text2('Wall HP: 10', {
size: 50,
fill: 0xFF0000
});
wallHealthText.anchor.set(0, 0);
wallHealthText.x = 150;
wallHealthText.y = 120;
LK.gui.topLeft.addChild(wallHealthText);
// Stage indicator display
var stageText = new Text2('Stage: 1', {
size: 50,
fill: 0x00FF00
});
stageText.anchor.set(1, 0);
stageText.x = -50;
stageText.y = 50;
LK.gui.topRight.addChild(stageText);
// Create wall
var wall = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 1,
x: 1024,
y: 1900
}));
// Create wall slots (9 defensive positions)
for (var i = 0; i < 9; i++) {
var slot = game.addChild(LK.getAsset('wallSlot', {
anchorX: 0.5,
anchorY: 0.5,
x: 200 + i * 200,
y: 1900 - 75
}));
wallSlots.push(slot);
}
// Create dice grid (3x3)
var diceGridStartX = 824; // Center on screen (1024 - 200)
var diceGridStartY = 2100;
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var slot = game.addChild(LK.getAsset('diceSlot', {
anchorX: 0.5,
anchorY: 0.5,
x: diceGridStartX + col * 200,
y: diceGridStartY + row * 200
}));
slot.isEmpty = true;
slot.gridIndex = row * 3 + col;
diceSlots.push(slot);
}
}
// Create trash area to the left of dice grid
trashArea = game.addChild(LK.getAsset('trashArea', {
anchorX: 0.5,
anchorY: 0.5,
x: 524,
// Left of dice grid
y: 2200
}));
// Buy button
var buyButton = game.addChild(LK.getAsset('buyButton', {
anchorX: 0.5,
anchorY: 0.5,
x: 1600,
y: 2200
}));
// Price display text
var priceText = new Text2('Price: 10', {
size: 40,
fill: 0xFFFFFF
});
priceText.anchor.set(0.5, 0);
priceText.x = 1600;
priceText.y = 2400;
game.addChild(priceText);
buyButton.down = function (x, y, obj) {
var currentPrice = 10 + dicesPurchased * 5;
if (gold >= currentPrice) {
buyDie();
}
};
function buyDie() {
var emptySlot = null;
for (var i = 0; i < diceSlots.length; i++) {
if (diceSlots[i].isEmpty) {
emptySlot = diceSlots[i];
break;
}
}
if (emptySlot) {
var currentPrice = 10 + dicesPurchased * 5;
gold -= currentPrice;
dicesPurchased++;
goldText.setText('Gold: ' + gold);
// Update price display for next purchase
var nextPrice = 10 + dicesPurchased * 5;
priceText.setText('Price: ' + nextPrice);
var dieTypes = ['green', 'yellow', 'red', 'orange', 'blue', 'black'];
var weights = [30, 30, 20, 20, 10, 10]; // Common, Common, Rare, Rare, Legendary, Legendary
var randomType = getWeightedRandom(dieTypes, weights);
var newDie = new Die(randomType, 1);
newDie.x = emptySlot.x;
newDie.y = emptySlot.y;
newDie.slotIndex = emptySlot.gridIndex;
dice.push(newDie);
game.addChild(newDie);
emptySlot.isEmpty = false;
emptySlot.occupiedBy = newDie;
// Create corresponding defender
createDefender(newDie);
}
}
function getWeightedRandom(items, weights) {
var totalWeight = 0;
for (var i = 0; i < weights.length; i++) {
totalWeight += weights[i];
}
var random = Math.random() * totalWeight;
var currentWeight = 0;
for (var i = 0; i < items.length; i++) {
currentWeight += weights[i];
if (random <= currentWeight) {
return items[i];
}
}
return items[0];
}
function createDefender(die) {
var defender = new Defender(die.type, die.level);
var wallSlot = wallSlots[die.slotIndex % 9];
defender.x = wallSlot.x;
defender.y = wallSlot.y;
defender.linkedDie = die;
defenders.push(defender);
game.addChild(defender);
}
function spawnEnemy() {
// Check total enemy limit
if (totalEnemyCount >= 20) {
return; // Don't spawn if at maximum enemy count
}
var enemyType = 'troll';
var enemiesToSpawn = 1;
var spawnPattern = 'single';
// Stage 1: Only trolls (0-60 seconds) - slow spawn rate
if (gameTimer <= 3600) {
enemyType = 'troll';
spawnPattern = 'single';
}
// Stage 2: Trolls + skeletons (60-120 seconds) - faster spawn rate
else if (gameTimer <= 7200) {
enemyType = Math.random() < 0.6 ? 'troll' : 'skeleton';
spawnPattern = 'single';
}
// Stage 3: Trolls + knights (120-165 seconds) - trolls get more health
else if (gameTimer <= 9900) {
enemyType = Math.random() < 0.7 ? 'troll' : 'knight';
spawnPattern = 'single';
}
// Stage 4: Knights + witches + trolls (165-210 seconds)
else if (gameTimer <= 12600) {
var rand = Math.random();
if (rand < 0.4) enemyType = 'knight';else if (rand < 0.7 && witchCount < 4) enemyType = 'witch';else enemyType = 'troll';
spawnPattern = 'single';
}
// Stage 5: Golems in front, then knights behind, then witches (210+ seconds)
else {
var rand = Math.random();
if (rand < 0.3 && golemCount < 3) {
enemyType = 'golem';
spawnPattern = 'front';
} else if (rand < 0.7) {
enemyType = 'knight';
spawnPattern = 'behind';
} else if (witchCount < 4) {
enemyType = 'witch';
spawnPattern = 'back';
} else {
enemyType = 'knight';
spawnPattern = 'behind';
}
}
// Spawn enemies based on pattern
if (spawnPattern === 'single') {
var enemy = new Enemy(enemyType);
// Stage 3+: Give trolls and skeletons more health
if (gameTimer > 7200) {
if (enemyType === 'troll') {
enemy.health = 10;
enemy.maxHealth = 10;
enemy.children[1].setText(enemy.health.toString()); // Update health display
} else if (enemyType === 'skeleton') {
enemy.health = 15;
enemy.maxHealth = 15;
enemy.children[1].setText(enemy.health.toString()); // Update health display
}
}
enemy.x = Math.random() * 1688 + 180; // Random X position (avoiding 80px margins)
enemy.y = -100; // Start above screen
if (enemyType === 'witch') {
witchCount++;
// Spawn 2 skeletons with 25 health each in front of witch
for (var s = 0; s < 2; s++) {
var skeleton = new Enemy('skeleton');
skeleton.health = 25;
skeleton.maxHealth = 25;
skeleton.children[1].setText(skeleton.health.toString()); // Update health display
skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch
skeleton.y = enemy.y + 100; // Position skeletons in front of witch
skeleton.createdBy = enemy; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
enemy.skeletonCount++;
enemy.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
}
}
if (enemyType === 'golem') {
golemCount++;
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'front') {
// Spawn golem at front
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -100;
if (enemyType === 'golem') {
golemCount++;
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'behind') {
// Spawn knight behind golems
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -300; // Further back
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
} else if (spawnPattern === 'back') {
// Spawn witch at the back
var enemy = new Enemy(enemyType);
enemy.x = Math.random() * 1688 + 180;
enemy.y = -500; // Much further back
if (enemyType === 'witch') {
witchCount++;
// Spawn 2 skeletons with 25 health each in front of witch
for (var s = 0; s < 2; s++) {
var skeleton = new Enemy('skeleton');
skeleton.health = 25;
skeleton.maxHealth = 25;
skeleton.children[1].setText(skeleton.health.toString()); // Update health display
skeleton.x = enemy.x + (s - 0.5) * 120; // Position skeletons to left and right of witch
skeleton.y = enemy.y + 100; // Position skeletons in front of witch
skeleton.createdBy = enemy; // Link skeleton to witch
enemies.push(skeleton);
game.addChild(skeleton);
enemy.skeletonCount++;
enemy.spawnedSkeletons.push(skeleton);
totalEnemyCount++;
}
}
totalEnemyCount++;
enemies.push(enemy);
game.addChild(enemy);
}
}
function handleMove(x, y, obj) {
if (draggedDie) {
draggedDie.x = x;
draggedDie.y = y;
}
}
function handleUp(x, y, obj) {
if (draggedDie) {
var merged = false;
var trashed = false;
var moved = false;
// Check if dropped on trash area
if (draggedDie.intersects(trashArea)) {
// Destroy die and give 5 gold
gold += 5;
goldText.setText('Gold: ' + gold);
// Remove die from slot
var slot = diceSlots[draggedDie.slotIndex];
slot.isEmpty = true;
slot.occupiedBy = null;
// Remove corresponding defender
for (var i = defenders.length - 1; i >= 0; i--) {
if (defenders[i].linkedDie === draggedDie) {
defenders[i].destroy();
defenders.splice(i, 1);
break;
}
}
// Remove die
draggedDie.destroy();
dice.splice(dice.indexOf(draggedDie), 1);
trashed = true;
}
if (!trashed) {
// Check if dropped on an empty slot
for (var i = 0; i < diceSlots.length; i++) {
var targetSlot = diceSlots[i];
if (targetSlot.isEmpty && draggedDie.intersects(targetSlot)) {
// Move die to new slot
var oldSlot = diceSlots[draggedDie.slotIndex];
oldSlot.isEmpty = true;
oldSlot.occupiedBy = null;
// Update new slot
targetSlot.isEmpty = false;
targetSlot.occupiedBy = draggedDie;
draggedDie.slotIndex = targetSlot.gridIndex;
draggedDie.x = targetSlot.x;
draggedDie.y = targetSlot.y;
draggedDie.originalPosition.x = targetSlot.x;
draggedDie.originalPosition.y = targetSlot.y;
// Move corresponding defender to new wall position
for (var j = 0; j < defenders.length; j++) {
if (defenders[j].linkedDie === draggedDie) {
var newWallSlot = wallSlots[draggedDie.slotIndex % 9];
defenders[j].x = newWallSlot.x;
defenders[j].y = newWallSlot.y;
break;
}
}
moved = true;
break;
}
}
}
if (!trashed && !moved) {
// Check for merge with other dice
for (var i = 0; i < dice.length; i++) {
var otherDie = dice[i];
if (otherDie !== draggedDie && otherDie.type === draggedDie.type && otherDie.level === draggedDie.level && draggedDie.intersects(otherDie)) {
// Merge dice
mergeDice(draggedDie, otherDie);
merged = true;
break;
}
}
if (!merged) {
// Return to original position
draggedDie.x = draggedDie.originalPosition.x;
draggedDie.y = draggedDie.originalPosition.y;
}
}
draggedDie = null;
}
}
function mergeDice(die1, die2) {
// Remove die1 from game
var slot1 = diceSlots[die1.slotIndex];
slot1.isEmpty = true;
slot1.occupiedBy = null;
// Remove corresponding defender
for (var i = defenders.length - 1; i >= 0; i--) {
if (defenders[i].linkedDie === die1) {
defenders[i].destroy();
defenders.splice(i, 1);
break;
}
}
die1.destroy();
dice.splice(dice.indexOf(die1), 1);
// Upgrade die2
die2.updateLevel(die2.level + 1);
// Update corresponding defender
for (var i = 0; i < defenders.length; i++) {
if (defenders[i].linkedDie === die2) {
defenders[i].level = die2.level;
break;
}
}
LK.getSound('merge').play();
}
game.move = handleMove;
game.up = handleUp;
game.update = function () {
// Update game timer
gameTimer++;
// Update stage indicator
var currentStage = 1;
if (gameTimer > 12600) {
currentStage = 5;
} else if (gameTimer > 9900) {
currentStage = 4;
} else if (gameTimer > 7200) {
currentStage = 3;
} else if (gameTimer > 3600) {
currentStage = 2;
}
stageText.setText('Stage: ' + currentStage);
// Progressive enemy spawning
enemySpawnTimer++;
var currentInterval = enemySpawnInterval;
// Calculate progressive spawn rate increase
var baseInterval = 600; // Start at 10 seconds
var minInterval = 60; // Minimum 1 second spawn rate
var reductionRate = gameTimer * 0.0001; // Gradual reduction over time
currentInterval = Math.max(minInterval, baseInterval - gameTimer * 0.02);
// Stage-based spawn intervals
// Stage 1: Slow spawning (0-60 seconds) - only trolls
if (gameTimer <= 3600) {
currentInterval = 200; // 3.33 seconds (reduced spawn rate)
}
// Stage 2: Moderate spawning (60-120 seconds) - trolls + skeletons
else if (gameTimer <= 7200) {
currentInterval = 105; // 1.75 seconds (reduced from 2.5)
}
// Stage 3: Fast spawning (120-165 seconds) - trolls + knights
else if (gameTimer <= 9900) {
currentInterval = 75; // 1.25 seconds (reduced from 1.5)
}
// Stage 4: Very fast spawning (165-210 seconds) - knights + witches + trolls
else if (gameTimer <= 12600) {
currentInterval = 45; // 0.75 seconds (reduced from 1)
}
// Stage 5: Maximum spawning (210+ seconds) - golems, knights, witches formation
else {
currentInterval = 30; // 0.5 seconds (reduced from 0.75)
}
if (enemySpawnTimer >= currentInterval) {
enemySpawnTimer = 0;
spawnEnemy();
}
// Update bullets and check collisions
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Check if bullet should be destroyed
if (bullet.shouldDestroy || bullet.y < -50 || bullet.y > 2732 || bullet.x < 0 || bullet.x > 2048) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with target or any enemy
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (bullet.intersects(enemy)) {
// Handle area damage for cannon and dark mage
if (bullet.bulletType === 'cannonBall') {
// Small area damage for cannon
var areaDamage = new AreaDamage(enemy.x, enemy.y, 80, bullet.damage, enemy);
areaDamage.lifetime = 120; // 2 seconds at 60fps
game.addChild(areaDamage);
} else if (bullet.bulletType === 'darkMagic') {
// Large area damage for dark mage
var areaDamage = new AreaDamage(enemy.x, enemy.y, 150, bullet.damage, enemy);
areaDamage.lifetime = 120; // 2 seconds at 60fps
game.addChild(areaDamage);
}
enemy.takeDamage(bullet.damage);
var enemyDied = enemy.health <= 0;
if (enemyDied) {
// If this is a skeleton created by a witch, update witch's count
if (enemy.type === 'skeleton' && enemy.createdBy) {
enemy.createdBy.skeletonCount--;
}
// If this is a witch, decrement witch count
if (enemy.type === 'witch') {
witchCount--;
}
// If this is a golem, decrement golem count
if (enemy.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
enemy.destroy();
enemies.splice(j, 1);
}
// Special behavior for blueMagic - continue to next enemy
if (bullet.bulletType === 'blueMagic') {
// Initialize hit count if not set
if (!bullet.hitCount) bullet.hitCount = 0;
bullet.hitCount++;
// Find next enemy to target (if this is the first hit)
if (bullet.hitCount === 1) {
var nextTarget = null;
var shortestDistance = Infinity;
for (var k = 0; k < enemies.length; k++) {
var nextEnemy = enemies[k];
if (nextEnemy !== enemy && nextEnemy.parent) {
var dx = nextEnemy.x - bullet.x;
var dy = nextEnemy.y - bullet.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < shortestDistance) {
shortestDistance = distance;
nextTarget = nextEnemy;
}
}
}
// Set new target if found
if (nextTarget) {
bullet.target = nextTarget;
} else {
// No more targets, destroy bullet
bullet.destroy();
bullets.splice(i, 1);
}
} else {
// Second hit, destroy bullet
bullet.destroy();
bullets.splice(i, 1);
}
} else {
// Regular bullet behavior - only destroy if enemy survived
if (!enemyDied) {
bullet.destroy();
bullets.splice(i, 1);
}
}
hitEnemy = true;
break;
}
}
}
// Update and cleanup area damage objects
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child.shouldDestroy) {
child.destroy();
}
}
// Update ranged projectiles
for (var i = rangedProjectiles.length - 1; i >= 0; i--) {
var projectile = rangedProjectiles[i];
if (projectile.shouldDestroy || projectile.y > 2000) {
projectile.destroy();
rangedProjectiles.splice(i, 1);
}
}
// Check if enemies reached the wall (only for melee enemies)
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.behavior !== 'ranged' && enemy.y >= 1770 && !enemy.hasAttackedWall) {
// Enemy attacks wall - reduce health and update display
enemy.attackWall();
// If this is a skeleton created by a witch, update witch's count
if (enemy.type === 'skeleton' && enemy.createdBy) {
enemy.createdBy.skeletonCount--;
}
// If this is a witch, decrement witch count
if (enemy.type === 'witch') {
witchCount--;
}
// If this is a golem, decrement golem count
if (enemy.type === 'golem') {
golemCount--;
}
// Decrement total enemy count
totalEnemyCount--;
// Remove enemy after attacking wall
enemy.destroy();
enemies.splice(i, 1);
}
}
};
// Start with one free die
buyDie();
×××× ×¢× ×ׄ ×קשת ×××××××Ŗ. In-Game asset. 2d. High contrast. No shadows
×ׄ ×©× ×§×©×Ŗ פ×× × ×××¤× ××¢××. In-Game asset. 2d. High contrast. No shadows
Blue circle magic attack. In-Game asset. 2d. High contrast. No shadows
Dice factory from the fantasy world. In-Game asset. 2d. High contrast. No shadows
Trash area for dice from fantasy. In-Game asset. 2d. High contrast. No shadows
Troll enemy. In-Game asset. 2d. High contrast. No shadows
Skeleton enemy. In-Game asset. 2d. High contrast. No shadows
Cannon from the fantasy. In-Game asset. 2d. High contrast. No shadows
From the bird view, top of the wall. In-Game asset. 2d. High contrast. No shadows
×¢×××× ×צ××¢ ××××Ø× ×¢× ×××× ×××ר××Ŗ ××§××¤× ×××Ŗ× ×××× ×× ×× ××××¢××. In-Game asset. 2d. High contrast. No shadows
×§××”× ××¤× ×××××××Ŗ. In-Game asset. 2d. High contrast. No shadows
×§××”× ×©××× ××××. In-Game asset. 2d. High contrast. No shadows
Black magic from fantasy. In-Game asset. 2d. High contrast. No shadows
×××©×¤× ×××××××Ŗ. In-Game asset. 2d. High contrast. No shadows
פרש ר××× ×¢× ×”××”. In-Game asset. 2d. High contrast. No shadows
×××× ×¢×©×× ×××× ×××××××Ŗ. In-Game asset. 2d. High contrast. No shadows
ש××× ×××× ××¢×פף. In-Game asset. 2d. High contrast. No shadows
×××”×§××ר ×××××××Ŗ. In-Game asset. 2d. High contrast. No shadows
×××× ×× ××Ŗ. In-Game asset. 2d. High contrast. No shadows
bullet. In-Game asset. 2d. High contrast. No shadows
רקע ××ש××§ ×¤× ×××× ××××× ××ש××× ××ש צ××× ×××”×רת ×× ×Ø×× ××× ××ר×× ×××××Ŗ ××ער××Ŗ ×××צע ××× ××©× × ×©××ש ××¢×××× ×× ××ש×× ×ר××§× ×××××. ×ש××ש ××Ŗ××Ŗ×× ×× ×××× × ×¢× ×§ ×©× ×¦×Ø×× ××××Ø× ××Ŗ××× × ×××× ××××¢×× ××§× ×× ××, ××¢××£ ×צ×פ×ר. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows