User prompt
תעשה שככל שעובר הזמן, קצב הייצור של המפלצות גדל. תגדיל טיפה את החיים של השדון המכשפה והגולם תעשה שהמכשפה, כל 2 שניות מייצרת לידה שלד ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
בתחילת המשחק, מיוצרים רק טרולים בקצב די איטי. אחרי כמות של זמן, מתחילים להיווצר בקצב קצת יותר מהיר. לאחר מכן, מתחילים להיווצר גם שלדים. לאחר מכן, פרשים שזזים מהר אבל יש להם חיים 1 בלבד. לאחר מכן, פרשים עם טרולים במקביל בכמות גדולה. לאחר מכן, מכשפות הולכות לאט, וכל שלוש שניות מיצרות לידן שלד. ואז מכשפות עם פרשים עם טרולים. לאחר מכן מתחילים להגיע גלמים, עם שדונים, לגלמים יש 50 חיים, לשדונים יש 25 חיים, למכשפה יש 20 חיים. שדונים יורים על החומה מרחוק. כל יצור אחר צריך להגיע פיזית לחומה, ומוריד חיים אחד בשנייה. יש לחומה 100 חיים ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
FI הקוביה הראשונה שקונים עולה 10, השנייה15, השלישית 20 וכן הלאה. כל קוביה עולה יותר מהקודמת מתחת לקניית הקוביה, מופיע המחיר הנוכחי
User prompt
טרול 5 חיים, שלד 10 חיים
User prompt
תאט את קצב הייצור של האויבים
User prompt
תן 2 נזק לכל קסם כחול שמשוגר תציג על הקרקע את הנזק האזורי ל-2 שניות, נזק אזורי קטן לתותח ונזק אזורי גדול לקוסם האפל ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
קוביות צהובות וירוקות נשארות כרגיל הלוחם של הקוביה האדומה, המוסקטיר, עושה נזק של 2 לכל כדור שלו הלוחם של הקוביה הכתומה, התותח, יורה משהו שעושה נזק אזורי קטן הקוביה של הקוסם הכחול, מסתובבת כל 2 שניות במקום 3 ועושה נזק של 2. הקוסם האפל, עושה נזק אזורי גדול
User prompt
תוריד את החיים של הטרולים ל-10, של השלדים ל-20
User prompt
הטרולים ב-20 שניות הראשונות נוצרים מאוד לאט. לאחר מכן הקצב טיפה מתגבר, לאחר מכן מתחילים גם להגיע שלדים
User prompt
אם משפרים קוביה: המספרים שלה הם +2. מה הכוונה, קוביה ברמה 2 היא מקבלת ערכים בין 3 ל-8 בהסתברות שווה. קוביה ברמה 3 מקבלת ערכים בין 5 ל-10
User prompt
תוריד את הטקסט מכפתור הקנייה ומכפתור הפח
User prompt
תוסיף משמאל ללוח 3 על 3 מקום, שאם גוררים אליו קוביה אז היא נהרסת ומביאה לך 5 זהב. לא משנה מה הרמה שלה ומימין, מתחת לכפתור של קניית קוביה, אותו תהפוך שיעלה 10 זהב, בניין
User prompt
קח את כפתור קניית הקוביה עוד ימינה תעשה שאם הוא אמור לירות 3 יריות, אז זה יירה אותם בפעימות ולא בבת אחת. בהפרש של 0.3 שניות אחד מהשני ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
הכמות שיצאה בקוביה, זה כמות היריות שהדמות בחומה תירה אם יצא 5, הוא יירה 5 כדורים. אם יצא 3 אז 3 כדורים. לכל דמות יש את מה שהיא יורה: קשת יורה חצים, לוחם יורה חניתות, מוסקטיר יורה כדורים, תותח יורה פגזים, קוסם שמים יורה קסם כחול, קוסם אפל יורה קסם אפל
User prompt
תמרכז את הלוח 3 על 3 תיקח את כפתור הרכישה של הקוביות ימינה הקוביות מתגלגלות אחת לשלוש שניות. מיד כשהן מסיימות להתגלגל מי שעל החומה יורה, וזהו. פעם הבאה שהוא יירה יהיה כשהן יתגלגלו שוב תוסיף אנימציה של גלגול כשהן מתגלגלות ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
תיקח גם את החומה וגם את הלוח 3 על 3 באותה כמות קדימה היריות של המגנים על החומה, עוקבות ופוגעות במדויק באויבים, לא פשוט עפות בקו ישר ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
תיקח את החומה עוד קדימה, קצת יותר ממקודם שים במרכז למטה את הלוח 3 על 3 של הקוביות
User prompt
את החומה והמקומות על החומה עם הדמויות תיקח טיפה קדימה את הכפתור של רכישת קוביה תשים ליד הלוח עם המקומות לקוביות, ואותו תשים כמו שהוא עכשיו למטה אבל במרכז
Code edit (1 edits merged)
Please save this source code
User prompt
Village Defense: Dice Tower
Initial prompt
משחק הגנה על הכפר בתחתית המסך יש חומה עם 9 מקומות לדמויות הגנתיות מתחת לחומה, יש לוח בגודל 3 על 3 של מקומות לשים בהם קוביות כל קוביה שנוצרת בלוח, יוצרת דמות על החומה ישנן 6 סוגים של קוביות: ירוקה שמייצרת קשת על החומה, צהוב שמייצר לוחם חנית על החומה, אדום שמייצר מוסקטיר על החומה, כתום שמייצר תותח על החומה, כחול שמייצר קוסם שמים על החומה, שחור שמייצר קוסם אפל על החומה ירוק וצהוב הן קוביות רגילות, אדום וכתום קוביות נדירות, כחול ושחור קוביות אגדיות וההסתברות שכל אחת תיווצר היא בהתאם כל 3 שניות הקוביה מתגלגלת. ברמה אחד, יכול לצאת בהסתברות שווה בקוביה 1,2,3,4,5,6 וזה כמות היריות שהלוחם על החומה יירה מתחילים עם 10 זהב, שמאפשר בעצם לקנות קוביה אחת והיא נוצרת באחד מהתשע מקומות ומייצרת מגן על החומה מגיעים אויבים, וכל אויב שמת מביא כמות מסוימת של זהב רנדומלית וככה ניתן לקנות עוד קוביות. אם יש לך 2 קוביות בצבע ודרגה זהה, ניתן לאחד אותן על ידי גרירה של קוביה אחת על האחררת, ובכך להעלות אותה רמה בהתחלה נעשה שמגיעים אויבים טרולים והולכים לכיוון החומה.
/**** * 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