User prompt
if two bullet collide with each other make a big explosion animation ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
enemy shoots the direction where the hero is.
User prompt
hero shoots the direction where the enemy come from. and slow down hero's shoot rate
User prompt
hero shoots the direction where the enemy come from.
User prompt
enemies are not coming after 720 points. solve this bug
User prompt
after every 2 boss monster give the hero a one shot rpg ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
put extra health icons on right side of extra health writing
User prompt
put extra healt icons in front of extra health writing
User prompt
add explosion effect when bullet hits to the enemy ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
make enemies faster after every 300 points
User prompt
send two more enemies afte every 200 points
User prompt
add a boss enemy after kill every ten enemies
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'addChild')' in or related to this line: 'LK.gui.topleft.addChild(healthBarBg);' Line Number: 223
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'addChild')' in or related to this line: 'LK.gui.topright.addChild(healthBarBg);' Line Number: 223
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'addChild')' in or related to this line: 'LK.gui.topright.addChild(healthBarBg);' Line Number: 223
Code edit (4 edits merged)
Please save this source code
User prompt
add extra 3 health option and use when health bar comes 20 points. increase health by 50 points
User prompt
add a city background
User prompt
make obstacles undestroyable
User prompt
Please fix the bug: 'ReferenceError: heroBullets is not defined' in or related to this line: 'for (var i = heroBullets.length - 1; i >= 0; i--) {' Line Number: 378
Code edit (1 edits merged)
Please save this source code
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Boss = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('bossEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 1;
self.health = 10;
self.maxHealth = 10;
self.shootTimer = 0;
self.shootInterval = 60; // 1 second at 60fps
self.isBoss = true;
self.update = function () {
// Move toward player
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var moveX = dx / distance * self.speed * enemySpeedMultiplier * timeScale;
var moveY = dy / distance * self.speed * enemySpeedMultiplier * timeScale;
var newX = self.x + moveX;
var newY = self.y + moveY;
// Check collision with obstacles
var canMove = true;
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
if (newX + 75 > obstacle.x - 60 && newX - 75 < obstacle.x + 60 && newY + 75 > obstacle.y - 60 && newY - 75 < obstacle.y + 60) {
canMove = false;
break;
}
}
if (canMove) {
self.x = newX;
self.y = newY;
}
}
// Shoot at player more frequently
self.shootTimer += timeScale;
if (self.shootTimer >= self.shootInterval) {
self.shoot();
self.shootTimer = 0;
}
};
self.shoot = function () {
// Boss shoots 3 bullets in spread pattern
for (var i = -1; i <= 1; i++) {
var bullet = new EnemyBullet();
bullet.x = self.x + i * 30;
bullet.y = self.y;
enemyBullets.push(bullet);
game.addChild(bullet);
}
};
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
LK.effects.flashObject(self, 0xFFFFFF, 500);
LK.getSound('enemyHit').play();
return true; // Boss destroyed
}
// Flash red when taking damage
LK.effects.flashObject(self, 0xFF0000, 200);
return false;
};
return self;
});
var Enemy = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 2;
self.health = 1;
self.shootTimer = 0;
self.shootInterval = 120; // 2 seconds at 60fps
self.update = function () {
// Move toward player
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
var moveX = dx / distance * self.speed * enemySpeedMultiplier * timeScale;
var moveY = dy / distance * self.speed * enemySpeedMultiplier * timeScale;
var newX = self.x + moveX;
var newY = self.y + moveY;
// Check collision with obstacles
var canMove = true;
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
if (newX + 30 > obstacle.x - 60 && newX - 30 < obstacle.x + 60 && newY + 30 > obstacle.y - 60 && newY - 30 < obstacle.y + 60) {
canMove = false;
break;
}
}
if (canMove) {
self.x = newX;
self.y = newY;
}
}
// Shoot at player
self.shootTimer += timeScale;
if (self.shootTimer >= self.shootInterval) {
self.shoot();
self.shootTimer = 0;
}
};
self.shoot = function () {
var bullet = new EnemyBullet();
bullet.x = self.x;
bullet.y = self.y;
enemyBullets.push(bullet);
game.addChild(bullet);
};
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
LK.effects.flashObject(self, 0xFFFFFF, 200);
LK.getSound('enemyHit').play();
return true; // Enemy destroyed
}
return false;
};
return self;
});
var EnemyBullet = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('enemyBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 6;
self.damage = 1;
self.update = function () {
self.y += self.speed * timeScale;
};
return self;
});
var Hero = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5
});
self.maxHealth = 100;
self.health = self.maxHealth;
self.shootTimer = 0;
self.shootInterval = 15; // Fast shooting
self.update = function () {
// Always shoot when enemies exist
if (enemies.length > 0) {
self.shootTimer += timeScale;
if (self.shootTimer >= self.shootInterval) {
self.shoot();
self.shootTimer = 0;
}
}
};
self.shoot = function () {
var bullet;
if (rpgAmmo > 0) {
bullet = new RPGBullet();
rpgAmmo--;
updateRPGDisplay();
} else {
bullet = new HeroBullet();
}
bullet.x = self.x;
bullet.y = self.y - 40;
heroBullets.push(bullet);
game.addChild(bullet);
LK.getSound('shoot').play();
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFF0000, 500);
LK.getSound('playerHit').play();
// Use extra health if health drops to 20 or below and we have uses available
if (self.health <= 20 && extraHealthUses > 0) {
self.health += 50;
if (self.health > self.maxHealth) {
self.health = self.maxHealth;
}
extraHealthUses--;
updateExtraHealthDisplay();
LK.effects.flashObject(self, 0x00FF00, 800); // Green flash for healing
}
updateHealthBar();
if (self.health <= 0) {
LK.showGameOver();
}
};
return self;
});
var HeroBullet = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('heroBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 12;
self.damage = 1;
self.update = function () {
self.y -= self.speed * timeScale;
};
return self;
});
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
var RPGBullet = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('heroBullet', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.tint = 0xFF0000; // Red color for RPG
graphics.scaleX = 2;
graphics.scaleY = 2;
self.speed = 8;
self.damage = 999; // One shot kill
self.update = function () {
self.y -= self.speed * timeScale;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// Game variables
var enemies = [];
var enemyBullets = [];
var heroBullets = [];
var obstacles = [];
var timeScale = 1.0;
var isBulletTimeActive = false;
var bulletTimeEnergy = 100;
var maxBulletTimeEnergy = 100;
var bulletTimeDecayRate = 0.5;
var bulletTimeRechargeRate = 0.1;
var bulletTimeSlowFactor = 0.2;
var dragNode = null;
var enemySpawnTimer = 0;
var enemySpawnInterval = 180; // 3 seconds
var waveNumber = 1;
var enemiesPerWave = 3;
var extraHealthUses = 3;
var maxExtraHealthUses = 3;
var enemiesKilled = 0;
var bossActive = false;
var lastScoreCheck = 0;
var enemySpeedMultiplier = 1.0;
var bossesKilled = 0;
var rpgAmmo = 0;
// UI elements
var scoreTxt = new Text2('0', {
size: 60,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 50;
var healthBarBg = LK.getAsset('healthBarBg', {
anchorX: 0,
anchorY: 0
});
var healthBar = LK.getAsset('healthBar', {
anchorX: 0,
anchorY: 0
});
var bulletTimeBarBg = LK.getAsset('bulletTimeBarBg', {
anchorX: 0,
anchorY: 0
});
var bulletTimeBar = LK.getAsset('bulletTimeBar', {
anchorX: 0,
anchorY: 0
});
LK.gui.topLeft.addChild(healthBarBg);
LK.gui.topLeft.addChild(healthBar);
LK.gui.topLeft.addChild(bulletTimeBarBg);
LK.gui.topLeft.addChild(bulletTimeBar);
healthBarBg.x = 120;
healthBarBg.y = 20;
healthBar.x = 122;
healthBar.y = 22;
bulletTimeBarBg.x = 120;
bulletTimeBarBg.y = 60;
bulletTimeBar.x = 122;
bulletTimeBar.y = 62;
// Add extra health display text first
var extraHealthText = new Text2('Extra Health: 3', {
size: 40,
fill: 0x00FF00
});
extraHealthText.anchor.set(0, 0);
LK.gui.topLeft.addChild(extraHealthText);
extraHealthText.x = 120;
extraHealthText.y = 100;
// Add extra health icons after text on the right side
var extraHealthIcons = [];
for (var i = 0; i < maxExtraHealthUses; i++) {
var icon = LK.getAsset('extraHealthIcon', {
anchorX: 0.5,
anchorY: 0.5
});
LK.gui.topLeft.addChild(icon);
icon.x = 380 + i * 40;
icon.y = 115;
extraHealthIcons.push(icon);
}
// Add RPG ammo display
var rpgText = new Text2('RPG: 0', {
size: 40,
fill: 0xFF0000
});
rpgText.anchor.set(0, 0);
LK.gui.topLeft.addChild(rpgText);
rpgText.x = 120;
rpgText.y = 140;
// Add city background
var cityBg = LK.getAsset('cityBackground', {
anchorX: 0,
anchorY: 0
});
game.addChild(cityBg);
// Initialize hero
hero = game.addChild(new Hero());
hero.x = 1024;
hero.y = 2200;
// Create obstacles
for (var i = 0; i < 6; i++) {
var obstacle = new Obstacle();
obstacle.x = 400 + i % 3 * 600;
obstacle.y = 800 + Math.floor(i / 3) * 600;
obstacles.push(obstacle);
game.addChild(obstacle);
}
// Update score display
function updateScore() {
scoreTxt.setText(LK.getScore());
}
// Update health bar
function updateHealthBar() {
var healthPercent = hero.health / hero.maxHealth;
healthBar.width = 400 * healthPercent;
}
// Update bullet time bar
function updateBulletTimeBar() {
var energyPercent = bulletTimeEnergy / maxBulletTimeEnergy;
bulletTimeBar.width = 300 * energyPercent;
}
// Update extra health display
function updateExtraHealthDisplay() {
extraHealthText.setText('Extra Health: ' + extraHealthUses);
for (var i = 0; i < extraHealthIcons.length; i++) {
extraHealthIcons[i].alpha = i < extraHealthUses ? 1.0 : 0.3;
}
}
// Update RPG display
function updateRPGDisplay() {
rpgText.setText('RPG: ' + rpgAmmo);
}
// Spawn enemy
function spawnEnemy() {
var enemy = new Enemy();
var side = Math.floor(Math.random() * 4);
switch (side) {
case 0:
// Top
enemy.x = Math.random() * 2048;
enemy.y = -50;
break;
case 1:
// Right
enemy.x = 2098;
enemy.y = Math.random() * 1000 + 100;
break;
case 2:
// Bottom
enemy.x = Math.random() * 2048;
enemy.y = -50;
break;
case 3:
// Left
enemy.x = -50;
enemy.y = Math.random() * 1000 + 100;
break;
}
// Scale enemy stats with wave number
enemy.speed = 2 + waveNumber * 0.3;
enemy.shootInterval = Math.max(60, 120 - waveNumber * 5);
enemies.push(enemy);
game.addChild(enemy);
}
// Spawn boss enemy
function spawnBoss() {
var boss = new Boss();
boss.x = 1024; // Center horizontally
boss.y = 200; // Top of screen
enemies.push(boss);
game.addChild(boss);
bossActive = true;
}
// Spawn additional enemies based on score
function spawnScoreBasedEnemies() {
for (var i = 0; i < 2; i++) {
spawnEnemy();
}
}
// Create explosion effect
function createExplosion(x, y) {
var explosion = LK.getAsset('heroBullet', {
anchorX: 0.5,
anchorY: 0.5
});
explosion.x = x;
explosion.y = y;
explosion.tint = 0xFF4400; // Orange color for explosion
explosion.scaleX = 0.1;
explosion.scaleY = 0.1;
game.addChild(explosion);
// Animate explosion - scale up and fade out
tween(explosion, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
explosion.destroy();
}
});
}
// Increase enemy speed every 300 points
function checkEnemySpeedIncrease(currentScore) {
var speedLevel = Math.floor(currentScore / 300);
var newSpeedMultiplier = 1.0 + speedLevel * 0.5;
if (newSpeedMultiplier > enemySpeedMultiplier) {
enemySpeedMultiplier = newSpeedMultiplier;
}
}
// Handle bullet time activation
function activateBulletTime() {
if (bulletTimeEnergy > 0) {
isBulletTimeActive = true;
timeScale = bulletTimeSlowFactor;
}
}
// Handle bullet time deactivation
function deactivateBulletTime() {
isBulletTimeActive = false;
timeScale = 1.0;
}
// Event handlers
function handleMove(x, y, obj) {
if (dragNode) {
var newX = x;
var newY = y;
// Keep hero within bounds
newX = Math.max(40, Math.min(2008, newX));
newY = Math.max(40, Math.min(2692, newY));
// Check collision with obstacles
var canMove = true;
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
var tempHero = {
x: newX,
y: newY,
width: 80,
height: 80
};
if (newX + 40 > obstacle.x - 60 && newX - 40 < obstacle.x + 60 && newY + 40 > obstacle.y - 60 && newY - 40 < obstacle.y + 60) {
canMove = false;
break;
}
}
if (canMove) {
dragNode.x = newX;
dragNode.y = newY;
}
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
dragNode = hero;
activateBulletTime();
handleMove(x, y, obj);
};
game.up = function (x, y, obj) {
dragNode = null;
deactivateBulletTime();
};
// Main game update loop
game.update = function () {
// Update bullet time energy
if (isBulletTimeActive) {
bulletTimeEnergy -= bulletTimeDecayRate;
if (bulletTimeEnergy <= 0) {
bulletTimeEnergy = 0;
deactivateBulletTime();
}
} else {
bulletTimeEnergy += bulletTimeRechargeRate;
if (bulletTimeEnergy > maxBulletTimeEnergy) {
bulletTimeEnergy = maxBulletTimeEnergy;
}
}
updateBulletTimeBar();
// Check for score-based enemy spawning every 200 points
var currentScore = LK.getScore();
if (currentScore >= lastScoreCheck + 200) {
spawnScoreBasedEnemies();
lastScoreCheck = Math.floor(currentScore / 200) * 200;
}
// Check for enemy speed increase every 300 points
checkEnemySpeedIncrease(currentScore);
// Spawn enemies (but not when boss is active)
if (!bossActive) {
enemySpawnTimer += timeScale;
if (enemySpawnTimer >= enemySpawnInterval) {
if (enemies.length < enemiesPerWave) {
spawnEnemy();
}
enemySpawnTimer = 0;
// Start new wave if no enemies left
if (enemies.length === 0) {
waveNumber++;
enemiesPerWave = Math.min(8, 3 + Math.floor(waveNumber / 2));
enemySpawnInterval = Math.max(60, 180 - waveNumber * 10);
}
}
}
// Update hero bullets
for (var i = heroBullets.length - 1; i >= 0; i--) {
var bullet = heroBullets[i];
if (bullet.lastY === undefined) {
bullet.lastY = bullet.y;
}
// Remove bullets that go off screen
if (bullet.lastY >= -20 && bullet.y < -20) {
bullet.destroy();
heroBullets.splice(i, 1);
continue;
}
// Check collision with enemies
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (bullet.intersects(enemy)) {
// Create explosion effect at enemy position
createExplosion(enemy.x, enemy.y);
if (enemy.takeDamage(bullet.damage)) {
// Enemy destroyed
enemy.destroy();
enemies.splice(j, 1);
// Award more points for boss
var points = enemy.isBoss ? 100 : 10;
LK.setScore(LK.getScore() + points);
updateScore();
// Track kills and spawn boss
if (!enemy.isBoss) {
enemiesKilled++;
if (enemiesKilled % 10 === 0 && !bossActive) {
spawnBoss();
}
} else {
bossActive = false;
bossesKilled++;
// Give RPG after every 2 bosses
if (bossesKilled % 2 === 0) {
rpgAmmo += 1;
updateRPGDisplay();
// Flash effect to show RPG gained
LK.effects.flashScreen(0xFF0000, 500);
}
}
// Refuel bullet time on enemy kill
bulletTimeEnergy += 20;
if (bulletTimeEnergy > maxBulletTimeEnergy) {
bulletTimeEnergy = maxBulletTimeEnergy;
}
}
bullet.destroy();
heroBullets.splice(i, 1);
hitEnemy = true;
break;
}
}
if (!hitEnemy) {
// Check collision with obstacles
for (var j = obstacles.length - 1; j >= 0; j--) {
var obstacle = obstacles[j];
if (bullet.intersects(obstacle)) {
bullet.destroy();
heroBullets.splice(i, 1);
hitEnemy = true;
break;
}
}
}
if (!hitEnemy) {
bullet.lastY = bullet.y;
}
}
// Update enemy bullets
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var bullet = enemyBullets[i];
if (bullet.lastY === undefined) {
bullet.lastY = bullet.y;
}
// Remove bullets that go off screen
if (bullet.lastY <= 2752 && bullet.y > 2752) {
bullet.destroy();
enemyBullets.splice(i, 1);
continue;
}
// Check collision with hero
if (bullet.intersects(hero)) {
hero.takeDamage(bullet.damage);
bullet.destroy();
enemyBullets.splice(i, 1);
continue;
}
// Check collision with obstacles
var hitObstacle = false;
for (var j = obstacles.length - 1; j >= 0; j--) {
var obstacle = obstacles[j];
if (bullet.intersects(obstacle)) {
bullet.destroy();
enemyBullets.splice(i, 1);
hitObstacle = true;
break;
}
}
if (!hitObstacle) {
bullet.lastY = bullet.y;
}
}
// Check enemy collision with hero
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.intersects(hero)) {
hero.takeDamage(1);
enemy.destroy();
enemies.splice(i, 1);
}
}
};
// Initialize UI
updateScore();
updateHealthBar();
updateBulletTimeBar();
updateExtraHealthDisplay();
updateRPGDisplay(); ===================================================================
--- original.js
+++ change.js
@@ -164,9 +164,16 @@
}
}
};
self.shoot = function () {
- var bullet = new HeroBullet();
+ var bullet;
+ if (rpgAmmo > 0) {
+ bullet = new RPGBullet();
+ rpgAmmo--;
+ updateRPGDisplay();
+ } else {
+ bullet = new HeroBullet();
+ }
bullet.x = self.x;
bullet.y = self.y - 40;
heroBullets.push(bullet);
game.addChild(bullet);
@@ -213,8 +220,24 @@
anchorY: 0.5
});
return self;
});
+var RPGBullet = Container.expand(function () {
+ var self = Container.call(this);
+ var graphics = self.attachAsset('heroBullet', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ graphics.tint = 0xFF0000; // Red color for RPG
+ graphics.scaleX = 2;
+ graphics.scaleY = 2;
+ self.speed = 8;
+ self.damage = 999; // One shot kill
+ self.update = function () {
+ self.y -= self.speed * timeScale;
+ };
+ return self;
+});
/****
* Initialize Game
****/
@@ -247,8 +270,10 @@
var enemiesKilled = 0;
var bossActive = false;
var lastScoreCheck = 0;
var enemySpeedMultiplier = 1.0;
+var bossesKilled = 0;
+var rpgAmmo = 0;
// UI elements
var scoreTxt = new Text2('0', {
size: 60,
fill: 0xFFFFFF
@@ -304,8 +329,17 @@
icon.x = 380 + i * 40;
icon.y = 115;
extraHealthIcons.push(icon);
}
+// Add RPG ammo display
+var rpgText = new Text2('RPG: 0', {
+ size: 40,
+ fill: 0xFF0000
+});
+rpgText.anchor.set(0, 0);
+LK.gui.topLeft.addChild(rpgText);
+rpgText.x = 120;
+rpgText.y = 140;
// Add city background
var cityBg = LK.getAsset('cityBackground', {
anchorX: 0,
anchorY: 0
@@ -343,8 +377,12 @@
for (var i = 0; i < extraHealthIcons.length; i++) {
extraHealthIcons[i].alpha = i < extraHealthUses ? 1.0 : 0.3;
}
}
+// Update RPG display
+function updateRPGDisplay() {
+ rpgText.setText('RPG: ' + rpgAmmo);
+}
// Spawn enemy
function spawnEnemy() {
var enemy = new Enemy();
var side = Math.floor(Math.random() * 4);
@@ -549,8 +587,16 @@
spawnBoss();
}
} else {
bossActive = false;
+ bossesKilled++;
+ // Give RPG after every 2 bosses
+ if (bossesKilled % 2 === 0) {
+ rpgAmmo += 1;
+ updateRPGDisplay();
+ // Flash effect to show RPG gained
+ LK.effects.flashScreen(0xFF0000, 500);
+ }
}
// Refuel bullet time on enemy kill
bulletTimeEnergy += 20;
if (bulletTimeEnergy > maxBulletTimeEnergy) {
@@ -626,5 +672,6 @@
// Initialize UI
updateScore();
updateHealthBar();
updateBulletTimeBar();
-updateExtraHealthDisplay();
\ No newline at end of file
+updateExtraHealthDisplay();
+updateRPGDisplay();
\ No newline at end of file
draw a man like max payne. In-Game asset. 2d. High contrast. No shadows
draw a mafia guy with gun. In-Game asset. 2d. High contrast. No shadows
draw a city road map. In-Game asset. 2d. High contrast. No shadows
draw a mafia guy like a monster with gun. In-Game asset. 2d. High contrast. No shadows
draw a skyscraper. In-Game asset. 2d. High contrast. No shadows
draw a pistol bullet 50 calibre. In-Game asset. 2d. High contrast. No shadows
draw a heart with pixel art. In-Game asset. 2d. High contrast. No shadows