User prompt
do the same with the boss enemy as big_zombie
User prompt
do the same with the fast enemy as zombie_dog
User prompt
rename ranged enemy as woman_zombie with the same way
User prompt
rename normal enemy as normal_zombie in codes,comments and assets
User prompt
rename normal enemy as normal_zombie
User prompt
rename boss enemy as big_zombie
User prompt
rename normal enemy as normal zombie, fast enemy as zombie_dog and ranged enemy as woman_zombie
User prompt
“Instead of calculating separate drop chances for each power-up, please change the logic so that when a drop occurs, the game randomly selects exactly one power-up to drop (according to their relative weights) and drops only that one.”
User prompt
Please change the Extra Shot logic so that all bullets in a volley are spawned first, and then heroBulletCount is decremented exactly once—after the entire volley—instead of once per bullet.
User prompt
Move the heroBulletCount-- call outside the extra‐bullet loop so ammo is only decremented once per shot, not once per extra bullet.
User prompt
Modify the ‘Extra Shot’ upgrade so that the additional projectiles it fires do not consume any ammo — only the base shot should decrement the magazine count.
User prompt
Prevent extra shot upgrades from decreasing the magazine
User prompt
do the same for the future selected extra shot options
User prompt
extra shots should not decrease the magazine
User prompt
add shoot count down text for showing how many shoots left before reload below hero which moving with him
User prompt
make hero reload every 20 shoot
User prompt
while reloading add "Reloading" text above hero which moving with him
User prompt
reduce enemy frequency by %50
User prompt
change ranged and fast enemies spawn rate to %3
User prompt
change ranged enemies spawn rate to %10 instead of every 5th wave
User prompt
“Update the collision/gem-drop logic so that only enemies with isBoss === true (or isFinalBoss === true) drop multiple XP gems; all other enemies should always drop exactly one XP gem, regardless of their hp value.”
User prompt
set normal enemies hitpoint to 2
User prompt
make normal enemies hitpoint 2
User prompt
Ensure that all enemies always drop experience gems at a fixed, constant rat independent of their hitpoints.
User prompt
break this connection and restore the gem drop rate as before
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // unique asset for attack speed powerup // AttackSpeedPowerup class (doubles attack speed for 5 seconds) var AttackSpeedPowerup = Container.expand(function () { var self = Container.call(this); // Use unique attack_speed_powerup asset var sprite = self.attachAsset('attack_speed_powerup', { anchorX: 0.5, anchorY: 0.5 }); // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(sprite.width, sprite.height) / 2; self.update = function () {}; return self; }); // Bullet class var Bullet = Container.expand(function () { var self = Container.call(this); var bulletSprite = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(bulletSprite.width, bulletSprite.height) / 2; self.speed = 9.5; self.dirX = 1; self.dirY = 0; // Set bullet pierce to default 1, or use Bullet.prototype.pierce if set by upgrades if (typeof Bullet.prototype.pierce === "undefined") { Bullet.prototype.pierce = 1; } self.pierce = Bullet.prototype.pierce; // Track enemies already hit by this bullet self._hitEnemies = []; self.update = function () { if (typeof game !== "undefined" && game._levelUpFrozen) { return; } // Homing logic: if enabled, adjust direction toward nearest enemy if (Bullet.prototype.homing && typeof enemies !== "undefined" && enemies.length > 0) { // Spread phase: if more than 1 bullet, spread out for a short time before homing if (typeof bullets !== "undefined" && bullets.length > 1) { if (typeof self._spreadTimer === "undefined") { // Each bullet gets a unique spread angle based on its index in bullets array var idx = 0; for (var bidx = 0; bidx < bullets.length; bidx++) { if (bullets[bidx] === self) { idx = bidx; break; } } var spreadTotal = bullets.length; var spreadAngle = Math.PI / 6; // 30 degrees total spread var baseAngle = Math.atan2(self.dirY, self.dirX); var angleOffset = -spreadAngle / 2 + (spreadTotal === 1 ? 0 : spreadAngle * idx / (spreadTotal - 1)); self._spreadTargetAngle = baseAngle + angleOffset; self._spreadTimer = 12; // frames to spread before homing } if (self._spreadTimer > 0) { // Lerp direction toward spread angle var currentAngle = Math.atan2(self.dirY, self.dirX); var targetAngle = self._spreadTargetAngle; // Shortest angle difference var da = targetAngle - currentAngle; while (da > Math.PI) { da -= Math.PI * 2; } while (da < -Math.PI) { da += Math.PI * 2; } var lerp = 0.25; var newAngle = currentAngle + da * lerp; self.dirX = Math.cos(newAngle); self.dirY = Math.sin(newAngle); self.rotation = newAngle; self._spreadTimer--; } else { // After spread, always home in on the nearest enemy (not unique assignment) var minDist = 99999; var nearest = null; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; // Exclude enemies already hit by this bullet if (self._hitEnemies.indexOf(e) !== -1) { continue; } var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = e; } } // If all are hit, just pick the nearest anyway if (!nearest) { minDist = 99999; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = e; } } } self._homingTarget = nearest; var target = self._homingTarget; if (target) { var dx = target.x - self.x; var dy = target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { // Homing strength controls how fast the bullet can turn (0.0 = no turn, 1.0 = instant turn) var homingStrength = typeof Bullet.prototype.homingStrength === "number" ? Bullet.prototype.homingStrength : 0.08; // default 0.08 for smoothness // Calculate current and target angles var currentAngle = Math.atan2(self.dirY, self.dirX); var targetAngle = Math.atan2(dy, dx); // Interpolate angle using angular lerp var da = targetAngle - currentAngle; while (da > Math.PI) { da -= Math.PI * 2; } while (da < -Math.PI) { da += Math.PI * 2; } var newAngle = currentAngle + da * homingStrength; // Update direction self.dirX = Math.cos(newAngle); self.dirY = Math.sin(newAngle); self.rotation = newAngle; } } } } else { // Only one bullet, home immediately var minDist = 99999; var nearest = null; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; // Exclude enemies already hit by this bullet if (self._hitEnemies.indexOf(e) !== -1) { continue; } var dx = e.x - self.x; var dy = e.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; nearest = e; } } if (nearest) { var dx = nearest.x - self.x; var dy = nearest.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { // Homing strength controls how fast the bullet can turn (0.0 = no turn, 1.0 = instant turn) var homingStrength = typeof Bullet.prototype.homingStrength === "number" ? Bullet.prototype.homingStrength : 0.08; // default 0.08 for smoothness // Calculate current and target angles var currentAngle = Math.atan2(self.dirY, self.dirX); var targetAngle = Math.atan2(dy, dx); // Interpolate angle using angular lerp var da = targetAngle - currentAngle; while (da > Math.PI) { da -= Math.PI * 2; } while (da < -Math.PI) { da += Math.PI * 2; } var newAngle = currentAngle + da * homingStrength; // Update direction self.dirX = Math.cos(newAngle); self.dirY = Math.sin(newAngle); self.rotation = newAngle; } } } } self.x += self.dirX * self.speed; self.y += self.dirY * self.speed; // Ricochet logic: if enabled, on hit, bounce to nearest enemy up to N times if (typeof self.ricochetLeft === "undefined" && Bullet.prototype.ricochet) { self.ricochetLeft = Bullet.prototype.ricochet; } if (typeof self.ricochetLeft !== "undefined" && self.ricochetLeft > 0 && typeof enemies !== "undefined") { // Ricochet handled in collision, see main game.update } }; return self; }); // Enemy class var Enemy = Container.expand(function (opts) { var self = Container.call(this); opts = opts || {}; self.isBoss = !!opts.isBoss; self.isFinalBoss = !!opts.isFinalBoss; // allow explicit final boss flag // Preload both right and left sprites, only one visible at a time if (self.isBoss) { // Use special assets for Final Boss if (self.isFinalBoss) { var bossSpriteRight = self.attachAsset('Finalboss_right', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteRight.assetId = 'Finalboss_right'; var bossSpriteLeft = self.attachAsset('Finalboss_left', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteLeft.assetId = 'Finalboss_left'; bossSpriteLeft.visible = false; // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(bossSpriteRight.width, bossSpriteRight.height) / 2; self.speed = 0.7; self.hp = 2 * 100; // Final Boss HP: 100x normal enemy HP self._bossSpriteRight = bossSpriteRight; self._bossSpriteLeft = bossSpriteLeft; } else { var bossSpriteRight = self.attachAsset('boss_enemy', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteRight.assetId = 'boss_enemy'; var bossSpriteLeft = self.attachAsset('boss_enemy_left', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteLeft.assetId = 'boss_enemy_left'; bossSpriteLeft.visible = false; // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(bossSpriteRight.width, bossSpriteRight.height) / 2; self.speed = 0.7; self.hp = 2 * 10; // Boss HP: 10x normal enemy HP self._bossSpriteRight = bossSpriteRight; self._bossSpriteLeft = bossSpriteLeft; } } else { // If opts.fastEnemy is set, use fast enemy assets if (opts && opts.fastEnemy) { var enemySpriteRight = self.attachAsset('zombiedog_right', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteRight.assetId = 'zombiedog_right'; var enemySpriteLeft = self.attachAsset('zombiedog_left', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteLeft.assetId = 'zombiedog_left'; enemySpriteLeft.visible = false; // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(enemySpriteRight.width, enemySpriteRight.height) / 2; self.speed = 5; self._enemySpriteRight = enemySpriteRight; self._enemySpriteLeft = enemySpriteLeft; self.isFastEnemy = true; self.hp = 1; // Fast enemy HP } else { var enemySpriteRight = self.attachAsset('enemy_right', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteRight.assetId = 'enemy_right'; var enemySpriteLeft = self.attachAsset('enemy_left', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteLeft.assetId = 'enemy_left'; enemySpriteLeft.visible = false; // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(enemySpriteRight.width, enemySpriteRight.height) / 2; self.speed = 1; self.hp = 2; // Normal enemy HP self._enemySpriteRight = enemySpriteRight; self._enemySpriteLeft = enemySpriteLeft; self.isFastEnemy = false; } } self._lastFacingRight = true; // Track last facing direction self.update = function () { if (typeof game !== "undefined" && game._levelUpFrozen) { return; } var dx = hero.x - self.x; var dy = hero.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { self.x += dx / dist * self.speed; self.y += dy / dist * self.speed; } // Face enemy left/right depending on hero position, by toggling visibility var shouldFaceRight = hero.x > self.x; if (self.isBoss) { if (shouldFaceRight !== self._lastFacingRight) { self._bossSpriteRight.visible = shouldFaceRight; self._bossSpriteLeft.visible = !shouldFaceRight; self._lastFacingRight = shouldFaceRight; } } else { if (shouldFaceRight !== self._lastFacingRight) { self._enemySpriteRight.visible = shouldFaceRight; self._enemySpriteLeft.visible = !shouldFaceRight; self._lastFacingRight = shouldFaceRight; } } }; return self; }); // Experience Gem class (handles both normal and big XP gems) var Gem = Container.expand(function (opts) { var self = Container.call(this); // Default to normal gem self.type = opts && opts.type || 'xp'; if (self.type === 'big') { var gemSprite = self.attachAsset('big_xp_gem', { anchorX: 0.5, anchorY: 0.5 }); // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(gemSprite.width, gemSprite.height) / 2; } else { var gemSprite = self.attachAsset('xp_gem', { anchorX: 0.5, anchorY: 0.5 }); // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(gemSprite.width, gemSprite.height) / 2; } self.update = function () {}; return self; }); // HeartPowerup class (can artıran kalp) var HeartPowerup = Container.expand(function () { var self = Container.call(this); self.attachAsset('heart_powerup', { anchorX: 0.5, anchorY: 0.5 }); // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(self.children[0].width, self.children[0].height) / 2; self.update = function () {}; return self; }); // Hero class var Hero = Container.expand(function () { var self = Container.call(this); // Preload both right and left hero sprites, only one visible at a time var heroSpriteRight = self.attachAsset('hero_right', { anchorX: 0.5, anchorY: 0.5 }); heroSpriteRight.assetId = 'hero_right'; var heroSpriteLeft = self.attachAsset('hero_left', { anchorX: 0.5, anchorY: 0.5 }); heroSpriteLeft.assetId = 'hero_left'; heroSpriteLeft.visible = false; // Set radius to match asset size (max of width/height / 2) self.radius = Math.max(heroSpriteRight.width, heroSpriteRight.height) / 2; self.speed = 5; self.targetX = 1024; self.targetY = 1366; self.magnetActive = false; self.magnetDuration = 0; self.magnetRange = 300; self.magnetRangeBoosted = 800; self._lastFacingRight = true; // Track last facing direction self.update = function () { var dx = self.targetX - self.x; var dy = self.targetY - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 10) { var move = Math.min(self.speed, dist); var prevX = self.x; self.x += dx / dist * move; self.y += dy / dist * move; // Face hero left/right depending on movement direction, by toggling visibility var shouldFaceRight = self.x > prevX; if (shouldFaceRight !== self._lastFacingRight) { heroSpriteRight.visible = shouldFaceRight; heroSpriteLeft.visible = !shouldFaceRight; self._lastFacingRight = shouldFaceRight; } } // --- Magnet glow effect --- if (self.magnetActive) { // Make hero glow blue while magnet is active if (!self._magnetGlowActive) { LK.effects.flashObject(self, 0x00ffff, 60000); // long duration, will be reset when deactivated self._magnetGlowActive = true; } // Magnet duration is now synchronized with game time (ticksSurvived) if (typeof self.magnetEndTick === "number") { if (typeof ticksSurvived === "number" && ticksSurvived >= self.magnetEndTick) { self.magnetActive = false; self.magnetDuration = 0; self.magnetEndTick = undefined; } else { // Update magnetDuration for UI display if (typeof ticksSurvived === "number") { self.magnetDuration = self.magnetEndTick - ticksSurvived; } } } } else { self._magnetGlowActive = false; } // --- Red glow effect when life is 1 --- if (typeof heroLives !== "undefined" && heroLives === 1) { if (!self._redGlowActive) { LK.effects.flashObject(self, 0xff0000, 60000); // long duration, will be reset when life changes self._redGlowActive = true; } } else { if (self._redGlowActive) { // Remove red glow by flashing with no color (or a neutral color, e.g. white, for a short time) LK.effects.flashObject(self, 0xffffff, 100); self._redGlowActive = false; } } }; return self; }); // InvulnerabilityCircle class for hero invulnerability visual effect var InvulnerabilityCircle = Container.expand(function () { var self = Container.call(this); // Attach the invulnerability circle asset, centered var circle = self.attachAsset('invuln_circle', { anchorX: 0.5, anchorY: 0.5 }); // Set alpha to 0.25 (50% of previous 0.5, and lower than previous 0.35) circle.alpha = 0.25; // Make sure it's always above the hero sprite self.zIndex = 1; // Optionally animate alpha for a pulsing effect self._pulseDir = 1; self.update = function () { // Pulse alpha between 0.25 and 0.5 (lowered by 50%) if (circle.alpha === undefined) { circle.alpha = 0.25; } circle.alpha += 0.005 * self._pulseDir; if (circle.alpha > 0.5) { circle.alpha = 0.5; self._pulseDir = -1; } else if (circle.alpha < 0.25) { circle.alpha = 0.25; self._pulseDir = 1; } // Always follow hero position if (typeof hero !== "undefined") { self.x = hero.x; self.y = hero.y; } }; return self; }); // MagnetCircle class for hero magnet visual effect var MagnetCircle = Container.expand(function () { var self = Container.call(this); // Attach the magnet circle asset, centered var circle = self.attachAsset('magnet_circle', { anchorX: 0.5, anchorY: 0.5 }); // Set the size of the magnet effect asset to match the magnet effect area // Use hero.magnetActive to determine which range to use var magnetRange = typeof hero !== "undefined" && hero.magnetActive ? hero.magnetRangeBoosted : hero.magnetRange; circle.width = magnetRange * 2; circle.height = magnetRange * 2; // Make sure it's always below the hero sprite self.zIndex = -2; // Optionally animate alpha for a pulsing effect self._pulseDir = 1; self.update = function () { // Pulse alpha between 0.25 and 0.35 (reduced by 50%) if (circle.alpha === undefined) { circle.alpha = 0.25; } circle.alpha += 0.002 * self._pulseDir; if (circle.alpha > 0.35) { circle.alpha = 0.35; self._pulseDir = -1; } else if (circle.alpha < 0.25) { circle.alpha = 0.25; self._pulseDir = 1; } // Always follow hero position and update size to match magnet effect area if (typeof hero !== "undefined") { self.x = hero.x; self.y = hero.y; // Dynamically update the size to match the current magnet area var magnetRange = hero.magnetActive ? hero.magnetRangeBoosted : hero.magnetRange; circle.width = magnetRange * 2; circle.height = magnetRange * 2; } }; return self; }); // Powerup class (now only for magnet) var Powerup = Container.expand(function (opts) { var self = Container.call(this); self.isMagnet = opts && opts.isMagnet || false; if (self.isMagnet) { self.attachAsset('magnet_powerup', { anchorX: 0.5, anchorY: 0.5 }); } // Set radius to match asset size (max of width/height / 2) if (self.children.length > 0) { self.radius = Math.max(self.children[0].width, self.children[0].height) / 2; } else { self.radius = 30; } self.update = function () {}; return self; }); // RangedEnemy class: maintains distance from hero and fires projectiles var RangedEnemy = Container.expand(function () { var self = Container.call(this); // Attach unique ranged enemy asset var rangedSpriteRight = self.attachAsset('ranged_enemy_right', { anchorX: 0.5, anchorY: 0.5 }); rangedSpriteRight.assetId = 'ranged_enemy_right'; var rangedSpriteLeft = self.attachAsset('ranged_enemy_left', { anchorX: 0.5, anchorY: 0.5 }); rangedSpriteLeft.assetId = 'ranged_enemy_left'; rangedSpriteLeft.visible = false; self.radius = Math.max(rangedSpriteRight.width, rangedSpriteRight.height) / 2; self.speed = 1.2; self.hp = 2; // Ranged enemy HP self._lastFacingRight = true; self._fireCooldown = 0; self._fireInterval = 450; // fires every 7.5 seconds self._desiredDistance = 800; self.update = function () { if (typeof game !== "undefined" && game._levelUpFrozen) { return; } // --- Stop after advancing 50px from entry --- // Track initial position and total distance advanced after entering screen if (typeof self._entryState === "undefined") { // Determine entry direction and record initial position if (self.x <= 0) { self._entryState = { edge: "left", start: { x: self.x, y: self.y }, advanced: 0, stopped: false }; } else if (self.x >= 2048) { self._entryState = { edge: "right", start: { x: self.x, y: self.y }, advanced: 0, stopped: false }; } else if (self.y <= 0) { self._entryState = { edge: "top", start: { x: self.x, y: self.y }, advanced: 0, stopped: false }; } else if (self.y >= 2732) { self._entryState = { edge: "bottom", start: { x: self.x, y: self.y }, advanced: 0, stopped: false }; } else { // Fallback: treat as already inside, allow movement self._entryState = { edge: "unknown", start: { x: self.x, y: self.y }, advanced: 0, stopped: false }; } } var dx = hero.x - self.x; var dy = hero.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); var canMove = true; if (!self._entryState.stopped) { // Calculate intended movement var moveDist = dist > 0 ? self.speed : 0; var moveX = dx / dist * moveDist; var moveY = dy / dist * moveDist; // Predict new position var nextX = self.x + moveX; var nextY = self.y + moveY; // Calculate how far we've advanced from entry point (projected along entry direction) var advanced = 0; if (self._entryState.edge === "left" || self._entryState.edge === "right") { advanced = Math.abs(self.x - self._entryState.start.x); } else if (self._entryState.edge === "top" || self._entryState.edge === "bottom") { advanced = Math.abs(self.y - self._entryState.start.y); } else { // Fallback: use total distance from start var dx0 = self.x - self._entryState.start.x; var dy0 = self.y - self._entryState.start.y; advanced = Math.sqrt(dx0 * dx0 + dy0 * dy0); } // If next move would exceed 250px, clamp to exactly 250px and stop if (advanced < 250 && dist > 0) { var remaining = 250 - advanced; if (moveDist > remaining) { moveX = dx / dist * remaining; moveY = dy / dist * remaining; self._entryState.stopped = true; } self.x += moveX; self.y += moveY; } else { self._entryState.stopped = true; // Do not move further } canMove = !self._entryState.stopped; } else { // Already stopped, do not move canMove = false; } // Face left/right var shouldFaceRight = hero.x > self.x; if (shouldFaceRight !== self._lastFacingRight) { rangedSpriteRight.visible = shouldFaceRight; rangedSpriteLeft.visible = !shouldFaceRight; self._lastFacingRight = shouldFaceRight; } // Fire projectile at hero with no range restriction, but only after stopping movement if (self._entryState.stopped) { self._fireCooldown--; if (self._fireCooldown <= 0) { self._fireCooldown = self._fireInterval; // Fire 3 projectiles toward hero in a spread pattern if (typeof game !== "undefined" && typeof RangedEnemyProjectile === "function") { var angleToHero = Math.atan2(hero.y - self.y, hero.x - self.x); var spread = Math.PI / 6; // 30 degrees for (var s = -1; s <= 1; s++) { var angle = angleToHero + s * spread; var targetX = self.x + Math.cos(angle) * 100; var targetY = self.y + Math.sin(angle) * 100; var proj = new RangedEnemyProjectile(self.x, self.y, targetX, targetY); game.addChild(proj); if (typeof enemyProjectiles !== "undefined") { enemyProjectiles.push(proj); } } } } } // Update lastX, lastY for next frame (legacy, not used for movement anymore) self.lastX = self.x; self.lastY = self.y; }; return self; }); // RangedEnemyProjectile class (simple straight projectile) var RangedEnemyProjectile = Container.expand(function (startX, startY, targetX, targetY) { var self = Container.call(this); // Use bullet asset, but tint red for enemy var sprite = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); sprite.tint = 0xff3333; self.radius = Math.max(sprite.width, sprite.height) / 2; self.x = startX; self.y = startY; var dx = targetX - startX; var dy = targetY - startY; var dist = Math.sqrt(dx * dx + dy * dy); self.speed = 2; self.dirX = dist > 0 ? dx / dist : 1; self.dirY = dist > 0 ? dy / dist : 0; self.rotation = Math.atan2(self.dirY, self.dirX); self.update = function () { if (typeof game !== "undefined" && game._levelUpFrozen) { return; } self.x += self.dirX * self.speed; self.y += self.dirY * self.speed; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x1e1b1d }); /**** * Game Code ****/ // Example: Bullet.prototype.homingStrength = 0.08; // default is 0.08 for smooth, natural turning // Set Bullet.prototype.homingStrength to control how fast homing bullets turn (0.0 = no turn, 1.0 = instant turn) // fast enemy right // fast enemy left // Unique ranged enemy assets Bullet.prototype.homingStrength = 0.008; // Lower value for even smoother, slower homing // Add background image to the game scene var background = LK.getAsset('background', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, width: 2048, height: 2732 }); game.addChild(background); var hero; var heroLives = 5; var enemies = []; var bullets = []; var enemyProjectiles = []; // For RangedEnemy projectiles var gems = []; var powerups = []; var spawnTimer = 0; var spawnInterval = 90; var wave = 1; var xp = 0; var xpToLevel = 10; var level = 1; var dragging = false; var lastGameOver = false; var scoreTxt, xpTxt, levelTxt; var centerX = 2048 / 2; var centerY = 2732 / 2; // --- Bullet count and reload state --- var heroBulletCount = 10; var heroBulletMax = 10; var heroReloading = false; var heroReloadTimeout = null; // --- Reload UI --- var reloadTxt = new Text2('', { size: 54, fill: 0xFFD700, font: "Montserrat" }); reloadTxt.anchor.set(0.5, 0); reloadTxt.x = 2048 / 2; reloadTxt.y = 10 + 60 + 10; LK.gui.top.addChild(reloadTxt); // --- Option popup at game start --- var startOptionPopup; function showStartOptionPopup() { // Freeze all game logic and input game._levelUpFrozen = true; // Remove any previous popup if present if (typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent) { startOptionPopup.parent.removeChild(startOptionPopup); startOptionPopup = null; } startOptionPopup = new Container(); // Dim background var bg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.57, color: 0x5e5e5d // green }); bg.width = 1100; bg.height = 1500; bg.alpha = 1; bg.x = centerX; bg.y = centerY; // Removed outline (LK.effects.outline not supported) startOptionPopup.addChild(bg); // Title replaced with LeveledUp asset var leveledUpImg = LK.getAsset('LeveledUp', { anchorX: 0.5, anchorY: 0.25, x: centerX, y: centerY - 610 }); startOptionPopup.addChild(leveledUpImg); // Option vertical layout var optionStartY = centerY - 320; var optionSpacing = 220; // --- Upgrade Option Definitions --- var allUpgradeOptions = [{ label: 'Attack Speed', desc: 'Fire faster from the start!', color: 0xF7E967, onSelect: function onSelect() { autoAttackInterval = Math.max(6, autoAttackInterval - 18); hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: 'Ricochet', desc: 'Bullets bounces', color: 0x7BE495, onSelect: function onSelect() { Bullet.prototype.ricochet = 1; hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: 'Bullet +1', desc: 'Shoot an extra bullet', color: 0xFFB347, onSelect: function onSelect() { Bullet.prototype.extraBullets = 1; hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: 'Pierce +1', desc: 'Bullets pierce enemies', color: 0x7BE4FF, onSelect: function onSelect() { if (typeof Bullet.prototype.pierce === "undefined") { Bullet.prototype.pierce = 2; } else { Bullet.prototype.pierce += 1; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: 'Missile', desc: 'Bullets home in on enemies', color: 0xFF77FF, onSelect: function onSelect() { Bullet.prototype.homing = true; window._missileChosen = true; hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: typeof hero !== "undefined" && typeof hero._doubleShotCount === "number" ? 'Extra Shot +1' : 'Extra Shot', desc: 'Adds additional shot', color: 0xFFD700, onSelect: function onSelect() { if (typeof hero._doubleShotCount === "undefined") { hero._doubleShotCount = 2; hero._doubleShot = true; } else { hero._doubleShotCount += 1; hero._doubleShot = true; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }]; // --- Shuffle and pick 4 random options --- function shuffleArray(arr) { for (var i = arr.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } // Track if missile has been chosen if (typeof window._missileChosen === "undefined") { window._missileChosen = false; } var upgradeOptions = allUpgradeOptions.slice(); // Remove 'Missile' option if already chosen if (window._missileChosen) { upgradeOptions = upgradeOptions.filter(function (opt) { return opt.label !== 'Missile'; }); } shuffleArray(upgradeOptions); var selectedOptions = upgradeOptions.slice(0, 4); // --- Render 4 random options --- for (var i = 0; i < selectedOptions.length; i++) { var opt = selectedOptions[i]; var optContainer = new Container(); var optBg = LK.getAsset('Button', { anchorX: 0.5, anchorY: 0.5 }); optBg.width = 700; optBg.height = 200; optBg.alpha = 0.98; optBg.x = centerX; optBg.y = optionStartY + optionSpacing * i; optContainer.addChild(optBg); var optTxt = new Text2(opt.label, { size: 54, fill: opt.color, font: "Montserrat" }); optTxt.anchor.set(0.5, 0.5); optTxt.x = centerX; optTxt.y = optionStartY + optionSpacing * i - 25; optContainer.addChild(optTxt); var optDesc = new Text2(opt.desc, { size: 40, fill: "#fff", font: "Montserrat" }); optDesc.anchor.set(0.5, 0.5); optDesc.x = centerX; optDesc.y = optionStartY + optionSpacing * i + 35; optContainer.addChild(optDesc); optContainer.interactive = true; // Defensive: wrap onSelect to also close popup and resume game optContainer.down = function (opt) { return function (x, y, obj) { opt.onSelect(); if (startOptionPopup && startOptionPopup.parent) { startOptionPopup.parent.removeChild(startOptionPopup); startOptionPopup = null; } game._levelUpFrozen = false; }; }(opt); startOptionPopup.addChild(optContainer); } // Add popup to game game.addChild(startOptionPopup); } // Show the popup at game start hero = new Hero(); hero.x = centerX; hero.y = centerY; game.addChild(hero); showStartOptionPopup(); // Removed scoreTxt, replaced by timer in top right var enemyKillCount = 0; var achievement50Shown = false; var achievement50Timeout = null; var achievement50Txt = new Text2('Achievement: 50 Kills!', { size: 80, fill: 0xFFD700, font: "Montserrat" }); achievement50Txt.anchor.set(0.5, 0); achievement50Txt.x = 2048 / 2; // achievement50Txt.y will be set after magnetTimerTxt is added and y is set achievement50Txt.visible = false; var enemyKillTxt = new Text2('Kills: 0', { size: 54, fill: 0xFFD700, font: "Montserrat" }); // Anchor bottom right enemyKillTxt.anchor.set(1, 1); // Place at bottom right corner, with 40px margin from right and bottom enemyKillTxt.x = -40; enemyKillTxt.y = -40; LK.gui.bottomRight.addChild(enemyKillTxt); game.addChild(achievement50Txt); var livesTxt = new Text2('Lives: ' + heroLives, { size: 54, fill: 0xFF5555, font: "Montserrat" }); livesTxt.anchor.set(1, 0); livesTxt.y = 10 + 60 - 10; livesTxt.x = -25; LK.gui.topRight.addChild(livesTxt); var timerTxt = new Text2('00:00', { size: 64, fill: "#fff", font: "Montserrat" }); timerTxt.anchor.set(0.5, 0); timerTxt.y = 55; LK.gui.top.addChild(timerTxt); var magnetTimerTxt = new Text2('', { size: 44, fill: 0x00FFFF, font: "Montserrat" }); magnetTimerTxt.anchor.set(0.5, 0); var attackSpeedTimerTxt = new Text2('', { size: 44, fill: 0xFFD700, font: "Montserrat" }); attackSpeedTimerTxt.anchor.set(0.5, 0); xpTxt = new Text2('XP: 0/10', { size: 44, fill: 0x7BE495, font: "Montserrat" }); xpTxt.anchor.set(1, 0); // Move XP text below the lives text (livesTxt.y + livesTxt.height + 8 for spacing), then 30px further down, then 70px further down, then 20px further down, then 10px up, then 60px up xpTxt.y = livesTxt.y + livesTxt.height + 8 - 30 + 30 + 70 + 20 - 10 - 60; xpTxt.x = -25 - 25 + 5; LK.gui.topRight.addChild(xpTxt); levelTxt = new Text2('Level: 1', { size: 44, fill: 0xF7E967, font: "Montserrat" }); levelTxt.anchor.set(0.5, 0); LK.gui.top.addChild(levelTxt); levelTxt.y = 150; // Now that levelTxt is defined, set magnetTimerTxt.y and add to LK.gui.top magnetTimerTxt.y = levelTxt.y + levelTxt.height + 10; LK.gui.top.addChild(magnetTimerTxt); // Set attackSpeedTimerTxt.y below magnetTimerTxt and add to LK.gui.top attackSpeedTimerTxt.y = magnetTimerTxt.y + magnetTimerTxt.height + 10; LK.gui.top.addChild(attackSpeedTimerTxt); // Now that magnetTimerTxt and attackSpeedTimerTxt are added and y is set, set achievement50Txt.y achievement50Txt.y = attackSpeedTimerTxt.y + attackSpeedTimerTxt.height + 20; function spawnEnemy() { var edge = Math.floor(Math.random() * 4); var x, y; var useRight = false; if (edge === 0) { x = Math.random() * 2048; y = -100; if (x > 1024) { useRight = true; } } else if (edge === 1) { x = 2048 + 100; y = Math.random() * 2732; useRight = true; } else if (edge === 2) { x = Math.random() * 2048; y = 2732 + 100; if (x > 1024) { useRight = true; } } else { x = -100; y = Math.random() * 2732; } // 10% chance to make this enemy a rare fast enemy (speed 5, not a boss/final boss) var isFast = false; if (Math.random() < 0.10) { isFast = true; } // Every 5th enemy, spawn a RangedEnemy instead var enemy; if (typeof window._rangedEnemyCounter === "undefined") { window._rangedEnemyCounter = 0; } window._rangedEnemyCounter++; if (window._rangedEnemyCounter % 5 === 0) { enemy = new RangedEnemy(); } else { enemy = new Enemy({ fastEnemy: isFast }); } // No need to set speed here, handled in Enemy class // Set initial facing direction based on spawn side if (useRight) { // Face right: right sprite visible, left sprite hidden if (enemy.children && enemy.children.length > 0) { for (var i = 0; i < enemy.children.length; i++) { var child = enemy.children[i]; if (child.assetId === 'enemy_right' || child.assetId === 'zombiedog_right') { child.visible = true; } if (child.assetId === 'enemy_left' || child.assetId === 'zombiedog_left') { child.visible = false; } } } enemy._lastFacingRight = true; } else { // Face left: left sprite visible, right sprite hidden if (enemy.children && enemy.children.length > 0) { for (var i = 0; i < enemy.children.length; i++) { var child = enemy.children[i]; if (child.assetId === 'enemy_right' || child.assetId === 'zombiedog_right') { child.visible = false; } if (child.assetId === 'enemy_left' || child.assetId === 'zombiedog_left') { child.visible = true; } } } enemy._lastFacingRight = false; } enemy.x = x; enemy.y = y; enemies.push(enemy); game.addChild(enemy); } function spawnGem(x, y, opts) { var gem = new Gem(opts); gem.x = x; gem.y = y; gems.push(gem); game.addChild(gem); } function spawnMagnetPowerup(x, y) { var powerup = new Powerup({ isMagnet: true }); // isMagnet parametresiyle oluştur powerup.x = x; powerup.y = y; powerups.push(powerup); game.addChild(powerup); } function spawnPowerup(x, y) { var powerup = new Powerup({ isMagnet: false }); // Diğer poweruplar powerup.x = x; powerup.y = y; powerups.push(powerup); // Remove and re-add to ensure it's above any background (if present) if (game.children && game.children.length > 0) { game.removeChild(powerup); game.addChild(powerup); } else { game.addChild(powerup); } } // Yeni: kalp powerup spawn fonksiyonu function spawnHeartPowerup(x, y) { var heart = new HeartPowerup(); heart.x = x; heart.y = y; powerups.push(heart); game.addChild(heart); } // Spawn attack speed powerup function spawnAttackSpeedPowerup(x, y) { var asp = new AttackSpeedPowerup(); asp.x = x; asp.y = y; powerups.push(asp); game.addChild(asp); } function fireBullet(dx, dy) { var bullet = new Bullet(); bullet.x = hero.x; bullet.y = hero.y; if (typeof Bullet.prototype.pierce === "undefined") { Bullet.prototype.pierce = 1; } bullet.pierce = Bullet.prototype.pierce; var dist = Math.sqrt(dx * dx + dy * dy); if (dist === 0) { bullet.dirX = 1; bullet.dirY = 0; } else { bullet.dirX = dx / dist; bullet.dirY = dy / dist; } bullet.rotation = Math.atan2(bullet.dirY, bullet.dirX); bullets.push(bullet); game.addChild(bullet); } function randomDir() { var angle = Math.random() * Math.PI * 2; return { x: Math.cos(angle), y: Math.sin(angle) }; } function dist2(a, b) { // 1) Mutlaka a ve b tanımlı olmalı if (!a || !b) { return 99999; } // 2) x,y özellikleri sayı değilse if (typeof a.x !== "number" || typeof a.y !== "number" || typeof b.x !== "number" || typeof b.y !== "number") { return 99999; } // 3) radius yoksa width/height'den ya da 0'dan tahmin et var aRadius = 0; if (a && typeof a.radius === "number") { aRadius = a.radius; } else if (a && typeof a.width === "number" && typeof a.height === "number") { aRadius = Math.max(a.width, a.height) / 2; } else { aRadius = 0; } var bRadius = 0; if (b && typeof b.radius === "number") { bRadius = b.radius; } else if (b && typeof b.width === "number" && typeof b.height === "number") { bRadius = Math.max(b.width, b.height) / 2; } else { bRadius = 0; } // 4) İki nokta arası mesafe var dx = a.x - b.x; var dy = a.y - b.y; return Math.sqrt(dx * dx + dy * dy); } game.down = function (x, y, obj) { if (game._levelUpFrozen) { dragging = false; return; } // Do not pause the game on left click/tap; just move the hero if not in the top-left menu area if (x < 100 && y < 100) { return; } // Prevent updating targetX/targetY if any popup is open (startOptionPopup or levelUpPopup) if (typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent) { return; } hero.targetX = x; hero.targetY = y; dragging = true; }; game.move = function (x, y, obj) { if (game._levelUpFrozen) { return; } // Prevent updating targetX/targetY if any popup is open (startOptionPopup or levelUpPopup) if (typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent) { return; } hero.targetX = x; hero.targetY = y; }; game.up = function (x, y, obj) { if (game._levelUpFrozen) { return; } dragging = false; }; var ticksSurvived = 0; var autoAttackTimer = 0; var autoAttackInterval = 96; game.update = function () { // --- Final Boss logic at 5 minutes --- if (typeof finalBossActive === "undefined") { finalBossActive = false; } if (typeof finalBossDefeated === "undefined") { finalBossDefeated = false; } if (typeof finalBoss === "undefined") { finalBoss = null; } if (typeof finalBossTriggered === "undefined") { finalBossTriggered = false; } if (typeof ticksSurvived !== "undefined" && !finalBossTriggered && Math.floor(ticksSurvived / 60) >= 120) { // 2 minutes reached, trigger Final Boss finalBossTriggered = true; finalBossActive = true; finalBossDefeated = false; // Destroy all enemies on screen for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] && enemies[i].parent) { enemies[i].destroy(); } enemies.splice(i, 1); } // Spawn Final Boss at top center (x = centerX, y = -200) finalBoss = new Enemy({ isBoss: true, isFinalBoss: true // use special asset }); finalBoss.x = centerX; finalBoss.y = -200; finalBoss.hp = 30 * 10; // 10x normal boss HP enemies.push(finalBoss); game.addChild(finalBoss); } // If Final Boss is active, halt wave spawns if (finalBossActive && finalBoss && typeof finalBoss.hp === "number" && finalBoss.hp > 0) { // Only update Final Boss and other logic, skip wave/enemy spawns // Allow all other update logic to run, but skip spawnTimer/wave logic below // Check if Final Boss is defeated if (finalBoss.hp <= 0) { finalBossActive = false; finalBossDefeated = true; finalBoss = null; // Resume normal wave spawning next frame } } else if (finalBossActive && (!finalBoss || finalBoss.hp <= 0)) { // Final Boss defeated, resume normal wave spawning finalBossActive = false; finalBossDefeated = true; finalBoss = null; } if (game._levelUpFrozen) { // Freeze all game logic and input while level up popup is active return; } // --- Invulnerability circle effect logic --- if (typeof hero !== "undefined") { // Track invuln circle instance globally if (typeof invulnCircle === "undefined") { invulnCircle = null; } var invulnActive = typeof hero._invulnerableUntilTick === "number" && typeof ticksSurvived === "number" && ticksSurvived < hero._invulnerableUntilTick; if (invulnActive) { if (!invulnCircle || !invulnCircle.parent) { invulnCircle = new InvulnerabilityCircle(); invulnCircle.x = hero.x; invulnCircle.y = hero.y; // Add above hero in display list var heroIdx = game.children.indexOf(hero); if (heroIdx !== -1) { game.addChildAt(invulnCircle, heroIdx + 1); } else { game.addChild(invulnCircle); } } // Always ensure hero is behind invulnCircle in display list if (invulnCircle && invulnCircle.parent && hero && hero.parent === game) { var invulnIdx = game.children.indexOf(invulnCircle); var heroIdx = game.children.indexOf(hero); if (heroIdx > invulnIdx - 1) { // Remove and re-add hero just before invulnCircle game.removeChild(hero); game.addChildAt(hero, invulnIdx); } } } else { if (invulnCircle && invulnCircle.parent) { invulnCircle.parent.removeChild(invulnCircle); invulnCircle = null; } } if (invulnCircle) { invulnCircle.update(); } // --- Magnet circle effect logic --- if (typeof magnetCircle === "undefined") { magnetCircle = null; } var magnetActive = hero.magnetActive && hero.magnetDuration > 0; if (magnetActive) { if (!magnetCircle || !magnetCircle.parent) { magnetCircle = new MagnetCircle(); magnetCircle.x = hero.x; magnetCircle.y = hero.y; // Add below hero in display list var heroIdx = game.children.indexOf(hero); if (heroIdx !== -1) { game.addChildAt(magnetCircle, heroIdx); } else { game.addChild(magnetCircle); } } } else { if (magnetCircle && magnetCircle.parent) { magnetCircle.parent.removeChild(magnetCircle); magnetCircle = null; } } if (magnetCircle) { magnetCircle.update(); } } hero.update(); for (var i = enemies.length - 1; i >= 0; i--) { var e = enemies[i]; e.update(); // Track lastWasTouchingHero for exact frame detection if (typeof e.lastWasTouchingHero === "undefined") { e.lastWasTouchingHero = false; } var isTouchingHero = dist2(e, hero) < e.radius + hero.radius; if (!e.lastWasTouchingHero && isTouchingHero) { // Check for hero invulnerability if (typeof hero._invulnerableUntilTick === "number" && typeof ticksSurvived === "number" && ticksSurvived < hero._invulnerableUntilTick) { // Do nothing, hero is invulnerable } else { LK.effects.flashScreen(0xff0000, 1000); heroLives--; livesTxt.setText('Lives: ' + heroLives); if (heroLives <= 0) { LK.showGameOver(); lastGameOver = true; } } } e.lastWasTouchingHero = isTouchingHero; } // --- RangedEnemy projectile update and collision --- for (var i = enemyProjectiles.length - 1; i >= 0; i--) { var proj = enemyProjectiles[i]; proj.update(); // Remove if offscreen if (proj.x < -100 || proj.x > 2148 || proj.y < -100 || proj.y > 2832) { proj.destroy(); enemyProjectiles.splice(i, 1); continue; } // Collision with hero if (dist2(proj, hero) < proj.radius + hero.radius) { // Check for hero invulnerability if (!(typeof hero._invulnerableUntilTick === "number" && typeof ticksSurvived === "number" && ticksSurvived < hero._invulnerableUntilTick)) { LK.effects.flashScreen(0xff0000, 1000); heroLives--; livesTxt.setText('Lives: ' + heroLives); if (heroLives <= 0) { LK.showGameOver(); lastGameOver = true; } } proj.destroy(); enemyProjectiles.splice(i, 1); continue; } } for (var i = bullets.length - 1; i >= 0; i--) { var b = bullets[i]; b.update(); // Remove bullet if it goes off-screen (outside visible area) if (b.x < -100 || b.x > 2148 || b.y < -100 || b.y > 2832) { b.destroy(); bullets.splice(i, 1); continue; } for (var j = enemies.length - 1; j >= 0; j--) { var e = enemies[j]; if (b && typeof b.radius === "number" && e && typeof e.radius === "number" && dist2(b, e) < b.radius + e.radius) { // Mark this enemy as hit by this bullet if (b._hitEnemies && b._hitEnemies.indexOf(e) === -1) { b._hitEnemies.push(e); } // Boss and normal enemy logic (bullets can pierce all enemy types) if (typeof e.hp === "number" && e.hp > 0) { e.hp -= 1; LK.effects.flashObject(e, 0xffffff, 120); if (e.hp <= 0) { // --- Powerup drop logic for boss --- var droppedPowerup = false; // Restore: Boss/final boss powerup drop rates are fixed by type, not HP var bossPowerupRate = e.isFinalBoss ? 0.5 : 0.25; if (Math.random() < bossPowerupRate) { // Pick one of the three powerups at random var powerupType = Math.floor(Math.random() * 3); if (powerupType === 0) { spawnMagnetPowerup(e.x, e.y); } else if (powerupType === 1) { spawnHeartPowerup(e.x, e.y); } else { spawnAttackSpeedPowerup(e.x, e.y); } droppedPowerup = true; } // Boss defeated: drop multiple big gems and normal gems ONLY if no powerup dropped if (!droppedPowerup) { // Restore: Boss always drops 1 big xp gem and 5 normal gems (not based on HP) var angle = Math.random() * Math.PI * 2; var dist = 60 + Math.random() * 40; var gemX = e.x + Math.cos(angle) * dist; var gemY = e.y + Math.sin(angle) * dist; spawnGem(gemX, gemY, { type: 'big' }); for (var drop = 0; drop < 5; drop++) { var angle = Math.random() * Math.PI * 2; var dist = 80 + Math.random() * 60; var gemX = e.x + Math.cos(angle) * dist; var gemY = e.y + Math.sin(angle) * dist; spawnGem(gemX, gemY); } } enemyKillCount++; e.destroy(); // Remove Final Boss reference if this was the Final Boss if (e.isFinalBoss) { finalBossActive = false; finalBossDefeated = true; finalBoss = null; } enemies.splice(j, 1); } } else { // --- Powerup drop logic for normal, fast, and ranged enemies --- // Restore: Drop rates are based only on enemy type, not HP var droppedPowerup = false; // 2% chance to drop magnet powerup if (Math.random() < 0.02) { spawnMagnetPowerup(e.x, e.y); droppedPowerup = true; } // 2% chance to drop heart powerup if (Math.random() < 0.02) { spawnHeartPowerup(e.x, e.y); droppedPowerup = true; } // 2% chance to drop attack speed powerup if (Math.random() < 0.02) { spawnAttackSpeedPowerup(e.x, e.y); droppedPowerup = true; } // Only drop XP gems if no powerup dropped if (!droppedPowerup) { var bigGemSpawned = false; if (Math.random() < 0.10) { // Drop big xp gem at 10% rate spawnGem(e.x, e.y, { type: 'big' }); bigGemSpawned = true; } // Offset the normal gem if big gem was spawned to avoid overlap if (bigGemSpawned) { var offsetAngle = Math.random() * Math.PI * 2; var offsetDist = 40; var gemX = e.x + Math.cos(offsetAngle) * offsetDist; var gemY = e.y + Math.sin(offsetAngle) * offsetDist; spawnGem(gemX, gemY); } else { spawnGem(e.x, e.y); } } enemyKillCount++; e.destroy(); enemies.splice(j, 1); } // Always decrement pierce on every enemy hit, regardless of enemy type (including bosses/final bosses) b.pierce -= 1; // Ricochet logic: if bullet has ricochetLeft, bounce to nearest enemy if (typeof b.ricochetLeft !== "undefined" && b.ricochetLeft > 0 && enemies.length > 1) { // Find nearest enemy that is not the one just hit var minDist = 99999; var nearest = null; for (var ricI = 0; ricI < enemies.length; ricI++) { var ricE = enemies[ricI]; // Exclude the just-hit enemy and any already-hit enemies if (ricE === e) { continue; } if (b._hitEnemies && b._hitEnemies.indexOf(ricE) !== -1) { continue; } var d = dist2(b, ricE); if (d < minDist) { minDist = d; nearest = ricE; } } if (nearest) { // Ricochet: set bullet position to current, aim at nearest enemy var dx = nearest.x - b.x; var dy = nearest.y - b.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { b.dirX = dx / dist; b.dirY = dy / dist; b.rotation = Math.atan2(b.dirY, b.dirX); b.ricochetLeft--; // Don't destroy or remove bullet, let it continue // Restore pierce for next hit b.pierce = Math.max(1, Bullet.prototype.pierce || 1); // Move bullet slightly toward new direction to avoid instant re-collision b.x += b.dirX * 10; b.y += b.dirY * 10; continue; } } } // If no ricochet, destroy as normal, but only if pierce <= 0 if (b.pierce <= 0) { b.destroy(); bullets.splice(i, 1); } break; } } } var _loop = function _loop() { g = gems[i]; // Set XP gem attraction area to 800 pixels when magnet is active, otherwise 150 xpAttractRange = hero.magnetActive ? 800 : 150; d = dist2(g, hero); if (d < xpAttractRange) { dx = hero.x - g.x; dy = hero.y - g.y; dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { g.x += dx / dist * 18; g.y += dy / dist * 18; } } if (g && typeof g.radius === "number" && hero && typeof hero.radius === "number" && dist2(g, hero) < g.radius + hero.radius) { // Big XP gem gives 5 XP, normal gives 1 if (g.type === 'big') { xp += 5; } else { xp += 1; } g.destroy(); gems.splice(i, 1); if (xp >= xpToLevel) { // --- Shuffle and pick 4 random options --- var shuffleArray = function shuffleArray(arr) { for (var i = arr.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }; level += 1; xp = 0; // Small automatic attack speed boost on every level up (less than upgrade option) autoAttackInterval = Math.max(6, autoAttackInterval - 3); // Easier XP curve: gentler growth for faster level up xpToLevel = 8 + level * 5 + Math.floor(level * level * 0.7); LK.effects.flashObject(hero, 0xf7e967, 600); // On level up, spawn a big XP gem near hero spawnGem(hero.x + (Math.random() - 0.5) * 200, hero.y + (Math.random() - 0.5) * 200, { type: 'big' }); // Pause game and show level up popup with two options // Remove any previous popup if present if (typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent) { levelUpPopup.parent.removeChild(levelUpPopup); levelUpPopup = null; } // Freeze all game logic and input game._levelUpFrozen = true; // Create popup container levelUpPopup = new Container(); // Dim background (match start popup) bg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.57, color: 0x008000 // green }); bg.width = 1100; bg.height = 1500; bg.alpha = 0.92; bg.x = centerX; bg.y = centerY; levelUpPopup.addChild(bg); // Title replaced with LeveledUp asset var leveledUpImg = LK.getAsset('LeveledUp', { anchorX: 0.5, anchorY: 0.25, x: centerX, y: centerY - 610 }); levelUpPopup.addChild(leveledUpImg); // Option vertical layout (match start popup) optionStartY = centerY - 320; optionSpacing = 220; // --- Upgrade Option Definitions (same as start popup, but update text for in-game context) --- allUpgradeOptions = [{ label: 'Attack Speed', desc: 'Fire faster every level!', color: 0xF7E967, onSelect: function onSelect() { autoAttackInterval = Math.max(6, autoAttackInterval - 12); hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: typeof Bullet.prototype.ricochet !== "undefined" && Bullet.prototype.ricochet >= 1 ? 'Ricochet +1 bounce' : 'Ricochet', desc: 'Bullets bounces', color: 0x7BE495, onSelect: function onSelect() { if (typeof Bullet.prototype.ricochet === "undefined" || Bullet.prototype.ricochet < 1) { Bullet.prototype.ricochet = 1; } else { Bullet.prototype.ricochet += 1; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: 'Bullet +1', desc: 'Shoot an extra bullet', color: 0xFFB347, onSelect: function onSelect() { if (typeof Bullet.prototype.extraBullets === "undefined") { Bullet.prototype.extraBullets = 1; } else { Bullet.prototype.extraBullets += 1; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: 'Pierce +1', desc: 'Bullets pierce enemies', color: 0x7BE4FF, onSelect: function onSelect() { if (typeof Bullet.prototype.pierce === "undefined") { Bullet.prototype.pierce = 2; } else { Bullet.prototype.pierce += 1; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: 'Missile', desc: 'Bullets home in on enemies', color: 0xFF77FF, onSelect: function onSelect() { Bullet.prototype.homing = true; window._missileChosen = true; hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }, { label: typeof hero !== "undefined" && typeof hero._doubleShotCount === "number" ? 'Extra Shot +1' : 'Extra Shot', desc: 'Adds additional shot', color: 0xFFD700, onSelect: function onSelect() { if (typeof hero._doubleShotCount === "undefined") { hero._doubleShotCount = 2; hero._doubleShot = true; } else { hero._doubleShotCount += 1; hero._doubleShot = true; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xffff00, 3000); } }]; // Track if missile has been chosen if (typeof window._missileChosen === "undefined") { window._missileChosen = false; } upgradeOptions = allUpgradeOptions.slice(); // Remove 'Missile' option if already chosen if (window._missileChosen) { upgradeOptions = upgradeOptions.filter(function (opt) { return opt.label !== 'Missile'; }); } shuffleArray(upgradeOptions); selectedOptions = upgradeOptions.slice(0, 4); // --- Render 4 random options with Button asset background --- for (i = 0; i < selectedOptions.length; i++) { opt = selectedOptions[i]; optContainer = new Container(); optBg = LK.getAsset('Button', { anchorX: 0.5, anchorY: 0.5 }); optBg.width = 700; optBg.height = 200; optBg.alpha = 0.98; optBg.x = centerX; optBg.y = optionStartY + optionSpacing * i; optContainer.addChild(optBg); optTxt = new Text2(opt.label, { size: 54, fill: opt.color, font: "Montserrat" }); optTxt.anchor.set(0.5, 0.5); optTxt.x = centerX; optTxt.y = optionStartY + optionSpacing * i - 25; optContainer.addChild(optTxt); optDesc = new Text2(opt.desc, { size: 40, fill: "#fff", font: "Montserrat" }); optDesc.anchor.set(0.5, 0.5); optDesc.x = centerX; optDesc.y = optionStartY + optionSpacing * i + 35; optContainer.addChild(optDesc); optContainer.interactive = true; optContainer.down = function (opt) { return function (x, y, obj) { opt.onSelect(); if (levelUpPopup && levelUpPopup.parent) { levelUpPopup.parent.removeChild(levelUpPopup); levelUpPopup = null; } game._levelUpFrozen = false; }; }(opt); levelUpPopup.addChild(optContainer); } // Add popup to game game.addChild(levelUpPopup); } } }, g, xpAttractRange, d, dx, dy, dist, levelUpPopup, bg, titleTxt, optionStartY, optionSpacing, allUpgradeOptions, upgradeOptions, selectedOptions, i, opt, optContainer, optBg, optTxt, optDesc; for (var i = gems.length - 1; i >= 0; i--) { _loop(); } for (var i = powerups.length - 1; i >= 0; i--) { var p = powerups[i]; // Powerup collection area: always 150px (not affected by magnet) var powerupAttractRange = 150; var d = dist2(p, hero); // Attract powerups (magnet, heart, and others) if within hero's collection area (150px), but NOT by magnet effect if (d < powerupAttractRange && d > p.radius + hero.radius) { // Move powerup toward hero (gentle attraction) var dx = hero.x - p.x; var dy = hero.y - p.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { p.x += dx / dist * 10; p.y += dy / dist * 10; } } // Magnet powerup pickup radius (same as attract range) if (p.isMagnet && d < p.radius + hero.radius) { hero.magnetActive = true; // Set magnetEndTick to 10 seconds (600 frames) from now, synchronized with game time if (typeof ticksSurvived === "number") { hero.magnetEndTick = ticksSurvived + 600; hero.magnetDuration = 600; } else { hero.magnetEndTick = undefined; hero.magnetDuration = 600; } hero._magnetGlowActive = false; // force re-apply blue glow in Hero.update LK.effects.flashObject(hero, 0x00ffff, 800); p.destroy(); powerups.splice(i, 1); } // Yeni: Kalp powerup toplama else if (p.constructor === HeartPowerup && d < p.radius + hero.radius) { heroLives++; if (heroLives > 5) { heroLives = 5; } livesTxt.setText('Lives: ' + heroLives); p.destroy(); powerups.splice(i, 1); } // Diğer poweruplar için (şu an sadece magnet ve heart var) else if (!p.isMagnet && !(p.constructor === HeartPowerup) && d < p.radius + hero.radius) { // Attack speed powerup pickup if (p.constructor === AttackSpeedPowerup) { // Set attack speed boost for 5 seconds (300 ticks), but do not stack duration, just reset to 5s hero._attackSpeedBoostEndTick = ticksSurvived + 300; LK.effects.flashObject(hero, 0xffff00, 800); } // Pick up other powerups if needed (future-proof) p.destroy(); powerups.splice(i, 1); } } // Handle attack speed boost effect if (typeof hero._attackSpeedBoostEndTick !== "undefined" && typeof ticksSurvived !== "undefined" && ticksSurvived < hero._attackSpeedBoostEndTick) { if (typeof hero._attackSpeedBoostActive === "undefined" || !hero._attackSpeedBoostActive) { hero._attackSpeedBoostActive = true; hero._originalAutoAttackInterval = autoAttackInterval; autoAttackInterval = Math.max(3, Math.floor(autoAttackInterval / 2)); } } else if (typeof hero._attackSpeedBoostActive !== "undefined" && hero._attackSpeedBoostActive) { hero._attackSpeedBoostActive = false; if (typeof hero._originalAutoAttackInterval !== "undefined") { autoAttackInterval = hero._originalAutoAttackInterval; } } autoAttackTimer++; if (autoAttackTimer >= autoAttackInterval) { autoAttackTimer = 0; // Block firing if reloading if (heroReloading) { // Do nothing, wait for reload to finish } else { var nearest = null, minDist = 99999; for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; var d = dist2(hero, e); if (d < minDist) { minDist = d; nearest = e; } } if (nearest) { var dx = nearest.x - hero.x; var dy = nearest.y - hero.y; // Remove automatic extra bullets from level; now controlled by upgrade if (typeof Bullet.prototype.extraBullets === "undefined") { Bullet.prototype.extraBullets = 0; } var extraBullets = Bullet.prototype.extraBullets; var totalBullets = 1 + extraBullets; var spread; var baseAngle = Math.atan2(dy, dx); // Double Shot logic: if enabled, fire multiple volleys in succession (not at the same time) var doubleShotActive = hero._doubleShot === true; var doubleShotCount = typeof hero._doubleShotCount === "number" && hero._doubleShotCount > 1 ? hero._doubleShotCount : doubleShotActive ? 2 : 1; // Count how many shots will be fired (1 volley or more for double shot) var shotsToFire = doubleShotActive ? doubleShotCount : 1; // If not enough bullets left, only fire as many as available if (heroBulletCount < shotsToFire) { shotsToFire = heroBulletCount; } // If no bullets left, start reload and block firing if (heroBulletCount <= 0) { if (!heroReloading) { heroReloading = true; reloadTxt.setText('Reloading...'); heroReloadTimeout = LK.setTimeout(function () { heroBulletCount = heroBulletMax; heroReloading = false; reloadTxt.setText(''); }, 1000); } } else { // Actually fire if (doubleShotActive) { // Fire multiple volleys in succession, number of volleys = shotsToFire (function fireDoubleShotVolley(volleyIdx) { if (heroBulletCount <= 0 || volleyIdx >= shotsToFire) { return; } var shotAngle = baseAngle; // Double shot bullets go in the same direction (no angle offset between volleys) var doubleShotAngleOffset = 0; shotAngle = baseAngle; if (totalBullets % 2 === 0 && totalBullets > 1) { // Even number of bullets: 2 center bullets go straight and parallel, others scatter var centerIdx1 = totalBullets / 2 - 1; var centerIdx2 = totalBullets / 2; var offsetDist = 30; var perpAngle = shotAngle + Math.PI / 2; for (var b = 0; b < totalBullets; b++) { if (b === centerIdx1 || b === centerIdx2) { var offset = (b === centerIdx1 ? -1 : 1) * offsetDist / 2; var bulletX = hero.x + Math.cos(perpAngle) * offset; var bulletY = hero.y + Math.sin(perpAngle) * offset; var dirX = Math.cos(shotAngle); var dirY = Math.sin(shotAngle); var bullet = new Bullet(); bullet.x = bulletX; bullet.y = bulletY; bullet.pierce = Bullet.prototype.pierce || 1; bullet.dirX = dirX; bullet.dirY = dirY; bullet.rotation = shotAngle; bullets.push(bullet); game.addChild(bullet); } else { // Scattered bullets var scatterCount = totalBullets - 2; var scatterIdx = b < centerIdx1 ? b : b - 2; var scatterSpread = Math.PI / 32 + (level - 2) * Math.PI / 48; if (scatterSpread > Math.PI / 4) { scatterSpread = Math.PI / 4; } var angle = shotAngle; if (scatterCount > 1) { angle = shotAngle - scatterSpread / 2 + scatterSpread * scatterIdx / (scatterCount - 1); } var dirX = Math.cos(angle); var dirY = Math.sin(angle); fireBullet(dirX, dirY); } } } else if (level > 2) { var spread = Math.PI / 32 + (level - 2) * Math.PI / 48; if (spread > Math.PI / 4) { spread = Math.PI / 4; } for (var b = 0; b < totalBullets; b++) { var angle = shotAngle; if (totalBullets > 1) { angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1); } var dirX = Math.cos(angle); var dirY = Math.sin(angle); fireBullet(dirX, dirY); } } else { var spread = Math.PI / 16; for (var b = 0; b < totalBullets; b++) { var angle = shotAngle; if (totalBullets > 1) { angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1); } var dirX = Math.cos(angle); var dirY = Math.sin(angle); fireBullet(dirX, dirY); } } heroBulletCount--; reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax); // If out of ammo after this shot, trigger reload if (heroBulletCount <= 0 && !heroReloading) { heroReloading = true; reloadTxt.setText('Reloading...'); heroReloadTimeout = LK.setTimeout(function () { heroBulletCount = heroBulletMax; heroReloading = false; reloadTxt.setText(''); }, 1000); } // Schedule next volley if more remain if (volleyIdx + 1 < shotsToFire && heroBulletCount > 0) { LK.setTimeout(function () { fireDoubleShotVolley(volleyIdx + 1); }, 300); // 300ms delay between shots } })(0); } else { // Single volley (normal fire) var shotAngle = baseAngle; if (totalBullets % 2 === 0 && totalBullets > 1) { var centerIdx1 = totalBullets / 2 - 1; var centerIdx2 = totalBullets / 2; var offsetDist = 30; var perpAngle = shotAngle + Math.PI / 2; for (var b = 0; b < totalBullets; b++) { if (b === centerIdx1 || b === centerIdx2) { var offset = (b === centerIdx1 ? -1 : 1) * offsetDist / 2; var bulletX = hero.x + Math.cos(perpAngle) * offset; var bulletY = hero.y + Math.sin(perpAngle) * offset; var dirX = Math.cos(shotAngle); var dirY = Math.sin(shotAngle); var bullet = new Bullet(); bullet.x = bulletX; bullet.y = bulletY; bullet.pierce = Bullet.prototype.pierce || 1; bullet.dirX = dirX; bullet.dirY = dirY; bullet.rotation = shotAngle; bullets.push(bullet); game.addChild(bullet); } else { var scatterCount = totalBullets - 2; var scatterIdx = b < centerIdx1 ? b : b - 2; var scatterSpread = Math.PI / 32 + (level - 2) * Math.PI / 48; if (scatterSpread > Math.PI / 4) { scatterSpread = Math.PI / 4; } var angle = shotAngle; if (scatterCount > 1) { angle = shotAngle - scatterSpread / 2 + scatterSpread * scatterIdx / (scatterCount - 1); } var dirX = Math.cos(angle); var dirY = Math.sin(angle); fireBullet(dirX, dirY); } } } else if (level > 2) { var spread = Math.PI / 32 + (level - 2) * Math.PI / 48; if (spread > Math.PI / 4) { spread = Math.PI / 4; } for (var b = 0; b < totalBullets; b++) { var angle = shotAngle; if (totalBullets > 1) { angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1); } var dirX = Math.cos(angle); var dirY = Math.sin(angle); fireBullet(dirX, dirY); } } else { var spread = Math.PI / 16; for (var b = 0; b < totalBullets; b++) { var angle = shotAngle; if (totalBullets > 1) { angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1); } var dirX = Math.cos(angle); var dirY = Math.sin(angle); fireBullet(dirX, dirY); } } heroBulletCount--; reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax); if (heroBulletCount <= 0 && !heroReloading) { heroReloading = true; reloadTxt.setText('Reloading...'); heroReloadTimeout = LK.setTimeout(function () { heroBulletCount = heroBulletMax; heroReloading = false; reloadTxt.setText(''); }, 1000); } } } } } } // Prevent enemy/wave spawn logic while Final Boss is active if (!finalBossActive) { spawnTimer++; if (spawnTimer >= spawnInterval) { spawnTimer = 0; var toSpawn = Math.max(1, Math.floor((1 + Math.floor(wave / 3)) / 4 * 0.75)); for (var i = 0; i < toSpawn; i++) { spawnEnemy(); } // Boss spawn logic: spawn boss every 30 waves if (typeof bossSpawnedWaves === "undefined") { bossSpawnedWaves = {}; } if (wave % 30 === 0 && !bossSpawnedWaves[wave]) { var boss = new Enemy({ isBoss: true }); // Spawn boss at a random edge var edge = Math.floor(Math.random() * 4); if (edge === 0) { boss.x = Math.random() * 2048; boss.y = -200; } else if (edge === 1) { boss.x = 2048 + 200; boss.y = Math.random() * 2732; } else if (edge === 2) { boss.x = Math.random() * 2048; boss.y = 2732 + 200; } else { boss.x = -200; boss.y = Math.random() * 2732; } enemies.push(boss); game.addChild(boss); bossSpawnedWaves[wave] = true; } wave++; spawnInterval = Math.max(24, 90 - Math.floor(wave / 2)); } } ticksSurvived++; // Removed scoreTxt.setText, timer is shown in top right only enemyKillTxt.setText('Kills: ' + enemyKillCount); if (!achievement50Shown && enemyKillCount >= 50) { achievement50Shown = true; achievement50Txt.visible = true; if (achievement50Timeout) { LK.clearTimeout(achievement50Timeout); } achievement50Timeout = LK.setTimeout(function () { achievement50Txt.visible = false; }, 2000); } xpTxt.setText('XP: ' + xp + '/' + xpToLevel); levelTxt.setText('Level: ' + level); var totalSeconds = Math.floor(ticksSurvived / 60); var minutes = Math.floor(totalSeconds / 60); var seconds = totalSeconds % 60; var minStr = minutes < 10 ? '0' + minutes : '' + minutes; var secStr = seconds < 10 ? '0' + seconds : '' + seconds; timerTxt.setText(minStr + ':' + secStr); // Only reset heroLives and livesTxt on game restart, not every frame if (lastGameOver && hero && hero.parent) { lastGameOver = false; heroLives = 5; if (heroLives > 5) { heroLives = 5; } if (livesTxt) { livesTxt.setText('Lives: ' + heroLives); } // Reset bullet count and reload state heroBulletCount = heroBulletMax; heroReloading = false; reloadTxt.setText(''); if (heroReloadTimeout) { LK.clearTimeout(heroReloadTimeout); heroReloadTimeout = null; } } // Magnet/Attack Speed timer display if (hero.magnetActive && hero.magnetDuration > 0) { var magnetSeconds = Math.ceil(hero.magnetDuration / 60); magnetTimerTxt.setText('Magnet: ' + magnetSeconds + 's'); } else { magnetTimerTxt.setText(''); } if (typeof hero._attackSpeedBoostEndTick !== "undefined" && typeof ticksSurvived !== "undefined" && ticksSurvived < hero._attackSpeedBoostEndTick) { var aspSeconds = Math.ceil((hero._attackSpeedBoostEndTick - ticksSurvived) / 60); attackSpeedTimerTxt.setText('Attack Speed: ' + aspSeconds + 's'); } else { attackSpeedTimerTxt.setText(''); } // Show ammo count if not reloading if (!heroReloading) { reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// unique asset for attack speed powerup
// AttackSpeedPowerup class (doubles attack speed for 5 seconds)
var AttackSpeedPowerup = Container.expand(function () {
var self = Container.call(this);
// Use unique attack_speed_powerup asset
var sprite = self.attachAsset('attack_speed_powerup', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(sprite.width, sprite.height) / 2;
self.update = function () {};
return self;
});
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletSprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(bulletSprite.width, bulletSprite.height) / 2;
self.speed = 9.5;
self.dirX = 1;
self.dirY = 0;
// Set bullet pierce to default 1, or use Bullet.prototype.pierce if set by upgrades
if (typeof Bullet.prototype.pierce === "undefined") {
Bullet.prototype.pierce = 1;
}
self.pierce = Bullet.prototype.pierce;
// Track enemies already hit by this bullet
self._hitEnemies = [];
self.update = function () {
if (typeof game !== "undefined" && game._levelUpFrozen) {
return;
}
// Homing logic: if enabled, adjust direction toward nearest enemy
if (Bullet.prototype.homing && typeof enemies !== "undefined" && enemies.length > 0) {
// Spread phase: if more than 1 bullet, spread out for a short time before homing
if (typeof bullets !== "undefined" && bullets.length > 1) {
if (typeof self._spreadTimer === "undefined") {
// Each bullet gets a unique spread angle based on its index in bullets array
var idx = 0;
for (var bidx = 0; bidx < bullets.length; bidx++) {
if (bullets[bidx] === self) {
idx = bidx;
break;
}
}
var spreadTotal = bullets.length;
var spreadAngle = Math.PI / 6; // 30 degrees total spread
var baseAngle = Math.atan2(self.dirY, self.dirX);
var angleOffset = -spreadAngle / 2 + (spreadTotal === 1 ? 0 : spreadAngle * idx / (spreadTotal - 1));
self._spreadTargetAngle = baseAngle + angleOffset;
self._spreadTimer = 12; // frames to spread before homing
}
if (self._spreadTimer > 0) {
// Lerp direction toward spread angle
var currentAngle = Math.atan2(self.dirY, self.dirX);
var targetAngle = self._spreadTargetAngle;
// Shortest angle difference
var da = targetAngle - currentAngle;
while (da > Math.PI) {
da -= Math.PI * 2;
}
while (da < -Math.PI) {
da += Math.PI * 2;
}
var lerp = 0.25;
var newAngle = currentAngle + da * lerp;
self.dirX = Math.cos(newAngle);
self.dirY = Math.sin(newAngle);
self.rotation = newAngle;
self._spreadTimer--;
} else {
// After spread, always home in on the nearest enemy (not unique assignment)
var minDist = 99999;
var nearest = null;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
// Exclude enemies already hit by this bullet
if (self._hitEnemies.indexOf(e) !== -1) {
continue;
}
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearest = e;
}
}
// If all are hit, just pick the nearest anyway
if (!nearest) {
minDist = 99999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearest = e;
}
}
}
self._homingTarget = nearest;
var target = self._homingTarget;
if (target) {
var dx = target.x - self.x;
var dy = target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
// Homing strength controls how fast the bullet can turn (0.0 = no turn, 1.0 = instant turn)
var homingStrength = typeof Bullet.prototype.homingStrength === "number" ? Bullet.prototype.homingStrength : 0.08; // default 0.08 for smoothness
// Calculate current and target angles
var currentAngle = Math.atan2(self.dirY, self.dirX);
var targetAngle = Math.atan2(dy, dx);
// Interpolate angle using angular lerp
var da = targetAngle - currentAngle;
while (da > Math.PI) {
da -= Math.PI * 2;
}
while (da < -Math.PI) {
da += Math.PI * 2;
}
var newAngle = currentAngle + da * homingStrength;
// Update direction
self.dirX = Math.cos(newAngle);
self.dirY = Math.sin(newAngle);
self.rotation = newAngle;
}
}
}
} else {
// Only one bullet, home immediately
var minDist = 99999;
var nearest = null;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
// Exclude enemies already hit by this bullet
if (self._hitEnemies.indexOf(e) !== -1) {
continue;
}
var dx = e.x - self.x;
var dy = e.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < minDist) {
minDist = dist;
nearest = e;
}
}
if (nearest) {
var dx = nearest.x - self.x;
var dy = nearest.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
// Homing strength controls how fast the bullet can turn (0.0 = no turn, 1.0 = instant turn)
var homingStrength = typeof Bullet.prototype.homingStrength === "number" ? Bullet.prototype.homingStrength : 0.08; // default 0.08 for smoothness
// Calculate current and target angles
var currentAngle = Math.atan2(self.dirY, self.dirX);
var targetAngle = Math.atan2(dy, dx);
// Interpolate angle using angular lerp
var da = targetAngle - currentAngle;
while (da > Math.PI) {
da -= Math.PI * 2;
}
while (da < -Math.PI) {
da += Math.PI * 2;
}
var newAngle = currentAngle + da * homingStrength;
// Update direction
self.dirX = Math.cos(newAngle);
self.dirY = Math.sin(newAngle);
self.rotation = newAngle;
}
}
}
}
self.x += self.dirX * self.speed;
self.y += self.dirY * self.speed;
// Ricochet logic: if enabled, on hit, bounce to nearest enemy up to N times
if (typeof self.ricochetLeft === "undefined" && Bullet.prototype.ricochet) {
self.ricochetLeft = Bullet.prototype.ricochet;
}
if (typeof self.ricochetLeft !== "undefined" && self.ricochetLeft > 0 && typeof enemies !== "undefined") {
// Ricochet handled in collision, see main game.update
}
};
return self;
});
// Enemy class
var Enemy = Container.expand(function (opts) {
var self = Container.call(this);
opts = opts || {};
self.isBoss = !!opts.isBoss;
self.isFinalBoss = !!opts.isFinalBoss; // allow explicit final boss flag
// Preload both right and left sprites, only one visible at a time
if (self.isBoss) {
// Use special assets for Final Boss
if (self.isFinalBoss) {
var bossSpriteRight = self.attachAsset('Finalboss_right', {
anchorX: 0.5,
anchorY: 0.5
});
bossSpriteRight.assetId = 'Finalboss_right';
var bossSpriteLeft = self.attachAsset('Finalboss_left', {
anchorX: 0.5,
anchorY: 0.5
});
bossSpriteLeft.assetId = 'Finalboss_left';
bossSpriteLeft.visible = false;
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(bossSpriteRight.width, bossSpriteRight.height) / 2;
self.speed = 0.7;
self.hp = 2 * 100; // Final Boss HP: 100x normal enemy HP
self._bossSpriteRight = bossSpriteRight;
self._bossSpriteLeft = bossSpriteLeft;
} else {
var bossSpriteRight = self.attachAsset('boss_enemy', {
anchorX: 0.5,
anchorY: 0.5
});
bossSpriteRight.assetId = 'boss_enemy';
var bossSpriteLeft = self.attachAsset('boss_enemy_left', {
anchorX: 0.5,
anchorY: 0.5
});
bossSpriteLeft.assetId = 'boss_enemy_left';
bossSpriteLeft.visible = false;
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(bossSpriteRight.width, bossSpriteRight.height) / 2;
self.speed = 0.7;
self.hp = 2 * 10; // Boss HP: 10x normal enemy HP
self._bossSpriteRight = bossSpriteRight;
self._bossSpriteLeft = bossSpriteLeft;
}
} else {
// If opts.fastEnemy is set, use fast enemy assets
if (opts && opts.fastEnemy) {
var enemySpriteRight = self.attachAsset('zombiedog_right', {
anchorX: 0.5,
anchorY: 0.5
});
enemySpriteRight.assetId = 'zombiedog_right';
var enemySpriteLeft = self.attachAsset('zombiedog_left', {
anchorX: 0.5,
anchorY: 0.5
});
enemySpriteLeft.assetId = 'zombiedog_left';
enemySpriteLeft.visible = false;
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(enemySpriteRight.width, enemySpriteRight.height) / 2;
self.speed = 5;
self._enemySpriteRight = enemySpriteRight;
self._enemySpriteLeft = enemySpriteLeft;
self.isFastEnemy = true;
self.hp = 1; // Fast enemy HP
} else {
var enemySpriteRight = self.attachAsset('enemy_right', {
anchorX: 0.5,
anchorY: 0.5
});
enemySpriteRight.assetId = 'enemy_right';
var enemySpriteLeft = self.attachAsset('enemy_left', {
anchorX: 0.5,
anchorY: 0.5
});
enemySpriteLeft.assetId = 'enemy_left';
enemySpriteLeft.visible = false;
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(enemySpriteRight.width, enemySpriteRight.height) / 2;
self.speed = 1;
self.hp = 2; // Normal enemy HP
self._enemySpriteRight = enemySpriteRight;
self._enemySpriteLeft = enemySpriteLeft;
self.isFastEnemy = false;
}
}
self._lastFacingRight = true; // Track last facing direction
self.update = function () {
if (typeof game !== "undefined" && game._levelUpFrozen) {
return;
}
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Face enemy left/right depending on hero position, by toggling visibility
var shouldFaceRight = hero.x > self.x;
if (self.isBoss) {
if (shouldFaceRight !== self._lastFacingRight) {
self._bossSpriteRight.visible = shouldFaceRight;
self._bossSpriteLeft.visible = !shouldFaceRight;
self._lastFacingRight = shouldFaceRight;
}
} else {
if (shouldFaceRight !== self._lastFacingRight) {
self._enemySpriteRight.visible = shouldFaceRight;
self._enemySpriteLeft.visible = !shouldFaceRight;
self._lastFacingRight = shouldFaceRight;
}
}
};
return self;
});
// Experience Gem class (handles both normal and big XP gems)
var Gem = Container.expand(function (opts) {
var self = Container.call(this);
// Default to normal gem
self.type = opts && opts.type || 'xp';
if (self.type === 'big') {
var gemSprite = self.attachAsset('big_xp_gem', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(gemSprite.width, gemSprite.height) / 2;
} else {
var gemSprite = self.attachAsset('xp_gem', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(gemSprite.width, gemSprite.height) / 2;
}
self.update = function () {};
return self;
});
// HeartPowerup class (can artıran kalp)
var HeartPowerup = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('heart_powerup', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(self.children[0].width, self.children[0].height) / 2;
self.update = function () {};
return self;
});
// Hero class
var Hero = Container.expand(function () {
var self = Container.call(this);
// Preload both right and left hero sprites, only one visible at a time
var heroSpriteRight = self.attachAsset('hero_right', {
anchorX: 0.5,
anchorY: 0.5
});
heroSpriteRight.assetId = 'hero_right';
var heroSpriteLeft = self.attachAsset('hero_left', {
anchorX: 0.5,
anchorY: 0.5
});
heroSpriteLeft.assetId = 'hero_left';
heroSpriteLeft.visible = false;
// Set radius to match asset size (max of width/height / 2)
self.radius = Math.max(heroSpriteRight.width, heroSpriteRight.height) / 2;
self.speed = 5;
self.targetX = 1024;
self.targetY = 1366;
self.magnetActive = false;
self.magnetDuration = 0;
self.magnetRange = 300;
self.magnetRangeBoosted = 800;
self._lastFacingRight = true; // Track last facing direction
self.update = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 10) {
var move = Math.min(self.speed, dist);
var prevX = self.x;
self.x += dx / dist * move;
self.y += dy / dist * move;
// Face hero left/right depending on movement direction, by toggling visibility
var shouldFaceRight = self.x > prevX;
if (shouldFaceRight !== self._lastFacingRight) {
heroSpriteRight.visible = shouldFaceRight;
heroSpriteLeft.visible = !shouldFaceRight;
self._lastFacingRight = shouldFaceRight;
}
}
// --- Magnet glow effect ---
if (self.magnetActive) {
// Make hero glow blue while magnet is active
if (!self._magnetGlowActive) {
LK.effects.flashObject(self, 0x00ffff, 60000); // long duration, will be reset when deactivated
self._magnetGlowActive = true;
}
// Magnet duration is now synchronized with game time (ticksSurvived)
if (typeof self.magnetEndTick === "number") {
if (typeof ticksSurvived === "number" && ticksSurvived >= self.magnetEndTick) {
self.magnetActive = false;
self.magnetDuration = 0;
self.magnetEndTick = undefined;
} else {
// Update magnetDuration for UI display
if (typeof ticksSurvived === "number") {
self.magnetDuration = self.magnetEndTick - ticksSurvived;
}
}
}
} else {
self._magnetGlowActive = false;
}
// --- Red glow effect when life is 1 ---
if (typeof heroLives !== "undefined" && heroLives === 1) {
if (!self._redGlowActive) {
LK.effects.flashObject(self, 0xff0000, 60000); // long duration, will be reset when life changes
self._redGlowActive = true;
}
} else {
if (self._redGlowActive) {
// Remove red glow by flashing with no color (or a neutral color, e.g. white, for a short time)
LK.effects.flashObject(self, 0xffffff, 100);
self._redGlowActive = false;
}
}
};
return self;
});
// InvulnerabilityCircle class for hero invulnerability visual effect
var InvulnerabilityCircle = Container.expand(function () {
var self = Container.call(this);
// Attach the invulnerability circle asset, centered
var circle = self.attachAsset('invuln_circle', {
anchorX: 0.5,
anchorY: 0.5
});
// Set alpha to 0.25 (50% of previous 0.5, and lower than previous 0.35)
circle.alpha = 0.25;
// Make sure it's always above the hero sprite
self.zIndex = 1;
// Optionally animate alpha for a pulsing effect
self._pulseDir = 1;
self.update = function () {
// Pulse alpha between 0.25 and 0.5 (lowered by 50%)
if (circle.alpha === undefined) {
circle.alpha = 0.25;
}
circle.alpha += 0.005 * self._pulseDir;
if (circle.alpha > 0.5) {
circle.alpha = 0.5;
self._pulseDir = -1;
} else if (circle.alpha < 0.25) {
circle.alpha = 0.25;
self._pulseDir = 1;
}
// Always follow hero position
if (typeof hero !== "undefined") {
self.x = hero.x;
self.y = hero.y;
}
};
return self;
});
// MagnetCircle class for hero magnet visual effect
var MagnetCircle = Container.expand(function () {
var self = Container.call(this);
// Attach the magnet circle asset, centered
var circle = self.attachAsset('magnet_circle', {
anchorX: 0.5,
anchorY: 0.5
});
// Set the size of the magnet effect asset to match the magnet effect area
// Use hero.magnetActive to determine which range to use
var magnetRange = typeof hero !== "undefined" && hero.magnetActive ? hero.magnetRangeBoosted : hero.magnetRange;
circle.width = magnetRange * 2;
circle.height = magnetRange * 2;
// Make sure it's always below the hero sprite
self.zIndex = -2;
// Optionally animate alpha for a pulsing effect
self._pulseDir = 1;
self.update = function () {
// Pulse alpha between 0.25 and 0.35 (reduced by 50%)
if (circle.alpha === undefined) {
circle.alpha = 0.25;
}
circle.alpha += 0.002 * self._pulseDir;
if (circle.alpha > 0.35) {
circle.alpha = 0.35;
self._pulseDir = -1;
} else if (circle.alpha < 0.25) {
circle.alpha = 0.25;
self._pulseDir = 1;
}
// Always follow hero position and update size to match magnet effect area
if (typeof hero !== "undefined") {
self.x = hero.x;
self.y = hero.y;
// Dynamically update the size to match the current magnet area
var magnetRange = hero.magnetActive ? hero.magnetRangeBoosted : hero.magnetRange;
circle.width = magnetRange * 2;
circle.height = magnetRange * 2;
}
};
return self;
});
// Powerup class (now only for magnet)
var Powerup = Container.expand(function (opts) {
var self = Container.call(this);
self.isMagnet = opts && opts.isMagnet || false;
if (self.isMagnet) {
self.attachAsset('magnet_powerup', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Set radius to match asset size (max of width/height / 2)
if (self.children.length > 0) {
self.radius = Math.max(self.children[0].width, self.children[0].height) / 2;
} else {
self.radius = 30;
}
self.update = function () {};
return self;
});
// RangedEnemy class: maintains distance from hero and fires projectiles
var RangedEnemy = Container.expand(function () {
var self = Container.call(this);
// Attach unique ranged enemy asset
var rangedSpriteRight = self.attachAsset('ranged_enemy_right', {
anchorX: 0.5,
anchorY: 0.5
});
rangedSpriteRight.assetId = 'ranged_enemy_right';
var rangedSpriteLeft = self.attachAsset('ranged_enemy_left', {
anchorX: 0.5,
anchorY: 0.5
});
rangedSpriteLeft.assetId = 'ranged_enemy_left';
rangedSpriteLeft.visible = false;
self.radius = Math.max(rangedSpriteRight.width, rangedSpriteRight.height) / 2;
self.speed = 1.2;
self.hp = 2; // Ranged enemy HP
self._lastFacingRight = true;
self._fireCooldown = 0;
self._fireInterval = 450; // fires every 7.5 seconds
self._desiredDistance = 800;
self.update = function () {
if (typeof game !== "undefined" && game._levelUpFrozen) {
return;
}
// --- Stop after advancing 50px from entry ---
// Track initial position and total distance advanced after entering screen
if (typeof self._entryState === "undefined") {
// Determine entry direction and record initial position
if (self.x <= 0) {
self._entryState = {
edge: "left",
start: {
x: self.x,
y: self.y
},
advanced: 0,
stopped: false
};
} else if (self.x >= 2048) {
self._entryState = {
edge: "right",
start: {
x: self.x,
y: self.y
},
advanced: 0,
stopped: false
};
} else if (self.y <= 0) {
self._entryState = {
edge: "top",
start: {
x: self.x,
y: self.y
},
advanced: 0,
stopped: false
};
} else if (self.y >= 2732) {
self._entryState = {
edge: "bottom",
start: {
x: self.x,
y: self.y
},
advanced: 0,
stopped: false
};
} else {
// Fallback: treat as already inside, allow movement
self._entryState = {
edge: "unknown",
start: {
x: self.x,
y: self.y
},
advanced: 0,
stopped: false
};
}
}
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var canMove = true;
if (!self._entryState.stopped) {
// Calculate intended movement
var moveDist = dist > 0 ? self.speed : 0;
var moveX = dx / dist * moveDist;
var moveY = dy / dist * moveDist;
// Predict new position
var nextX = self.x + moveX;
var nextY = self.y + moveY;
// Calculate how far we've advanced from entry point (projected along entry direction)
var advanced = 0;
if (self._entryState.edge === "left" || self._entryState.edge === "right") {
advanced = Math.abs(self.x - self._entryState.start.x);
} else if (self._entryState.edge === "top" || self._entryState.edge === "bottom") {
advanced = Math.abs(self.y - self._entryState.start.y);
} else {
// Fallback: use total distance from start
var dx0 = self.x - self._entryState.start.x;
var dy0 = self.y - self._entryState.start.y;
advanced = Math.sqrt(dx0 * dx0 + dy0 * dy0);
}
// If next move would exceed 250px, clamp to exactly 250px and stop
if (advanced < 250 && dist > 0) {
var remaining = 250 - advanced;
if (moveDist > remaining) {
moveX = dx / dist * remaining;
moveY = dy / dist * remaining;
self._entryState.stopped = true;
}
self.x += moveX;
self.y += moveY;
} else {
self._entryState.stopped = true;
// Do not move further
}
canMove = !self._entryState.stopped;
} else {
// Already stopped, do not move
canMove = false;
}
// Face left/right
var shouldFaceRight = hero.x > self.x;
if (shouldFaceRight !== self._lastFacingRight) {
rangedSpriteRight.visible = shouldFaceRight;
rangedSpriteLeft.visible = !shouldFaceRight;
self._lastFacingRight = shouldFaceRight;
}
// Fire projectile at hero with no range restriction, but only after stopping movement
if (self._entryState.stopped) {
self._fireCooldown--;
if (self._fireCooldown <= 0) {
self._fireCooldown = self._fireInterval;
// Fire 3 projectiles toward hero in a spread pattern
if (typeof game !== "undefined" && typeof RangedEnemyProjectile === "function") {
var angleToHero = Math.atan2(hero.y - self.y, hero.x - self.x);
var spread = Math.PI / 6; // 30 degrees
for (var s = -1; s <= 1; s++) {
var angle = angleToHero + s * spread;
var targetX = self.x + Math.cos(angle) * 100;
var targetY = self.y + Math.sin(angle) * 100;
var proj = new RangedEnemyProjectile(self.x, self.y, targetX, targetY);
game.addChild(proj);
if (typeof enemyProjectiles !== "undefined") {
enemyProjectiles.push(proj);
}
}
}
}
}
// Update lastX, lastY for next frame (legacy, not used for movement anymore)
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// RangedEnemyProjectile class (simple straight projectile)
var RangedEnemyProjectile = Container.expand(function (startX, startY, targetX, targetY) {
var self = Container.call(this);
// Use bullet asset, but tint red for enemy
var sprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
sprite.tint = 0xff3333;
self.radius = Math.max(sprite.width, sprite.height) / 2;
self.x = startX;
self.y = startY;
var dx = targetX - startX;
var dy = targetY - startY;
var dist = Math.sqrt(dx * dx + dy * dy);
self.speed = 2;
self.dirX = dist > 0 ? dx / dist : 1;
self.dirY = dist > 0 ? dy / dist : 0;
self.rotation = Math.atan2(self.dirY, self.dirX);
self.update = function () {
if (typeof game !== "undefined" && game._levelUpFrozen) {
return;
}
self.x += self.dirX * self.speed;
self.y += self.dirY * self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1e1b1d
});
/****
* Game Code
****/
// Example: Bullet.prototype.homingStrength = 0.08; // default is 0.08 for smooth, natural turning
// Set Bullet.prototype.homingStrength to control how fast homing bullets turn (0.0 = no turn, 1.0 = instant turn)
// fast enemy right
// fast enemy left
// Unique ranged enemy assets
Bullet.prototype.homingStrength = 0.008; // Lower value for even smoother, slower homing
// Add background image to the game scene
var background = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
width: 2048,
height: 2732
});
game.addChild(background);
var hero;
var heroLives = 5;
var enemies = [];
var bullets = [];
var enemyProjectiles = []; // For RangedEnemy projectiles
var gems = [];
var powerups = [];
var spawnTimer = 0;
var spawnInterval = 90;
var wave = 1;
var xp = 0;
var xpToLevel = 10;
var level = 1;
var dragging = false;
var lastGameOver = false;
var scoreTxt, xpTxt, levelTxt;
var centerX = 2048 / 2;
var centerY = 2732 / 2;
// --- Bullet count and reload state ---
var heroBulletCount = 10;
var heroBulletMax = 10;
var heroReloading = false;
var heroReloadTimeout = null;
// --- Reload UI ---
var reloadTxt = new Text2('', {
size: 54,
fill: 0xFFD700,
font: "Montserrat"
});
reloadTxt.anchor.set(0.5, 0);
reloadTxt.x = 2048 / 2;
reloadTxt.y = 10 + 60 + 10;
LK.gui.top.addChild(reloadTxt);
// --- Option popup at game start ---
var startOptionPopup;
function showStartOptionPopup() {
// Freeze all game logic and input
game._levelUpFrozen = true;
// Remove any previous popup if present
if (typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent) {
startOptionPopup.parent.removeChild(startOptionPopup);
startOptionPopup = null;
}
startOptionPopup = new Container();
// Dim background
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.57,
color: 0x5e5e5d // green
});
bg.width = 1100;
bg.height = 1500;
bg.alpha = 1;
bg.x = centerX;
bg.y = centerY;
// Removed outline (LK.effects.outline not supported)
startOptionPopup.addChild(bg);
// Title replaced with LeveledUp asset
var leveledUpImg = LK.getAsset('LeveledUp', {
anchorX: 0.5,
anchorY: 0.25,
x: centerX,
y: centerY - 610
});
startOptionPopup.addChild(leveledUpImg);
// Option vertical layout
var optionStartY = centerY - 320;
var optionSpacing = 220;
// --- Upgrade Option Definitions ---
var allUpgradeOptions = [{
label: 'Attack Speed',
desc: 'Fire faster from the start!',
color: 0xF7E967,
onSelect: function onSelect() {
autoAttackInterval = Math.max(6, autoAttackInterval - 18);
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: 'Ricochet',
desc: 'Bullets bounces',
color: 0x7BE495,
onSelect: function onSelect() {
Bullet.prototype.ricochet = 1;
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: 'Bullet +1',
desc: 'Shoot an extra bullet',
color: 0xFFB347,
onSelect: function onSelect() {
Bullet.prototype.extraBullets = 1;
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: 'Pierce +1',
desc: 'Bullets pierce enemies',
color: 0x7BE4FF,
onSelect: function onSelect() {
if (typeof Bullet.prototype.pierce === "undefined") {
Bullet.prototype.pierce = 2;
} else {
Bullet.prototype.pierce += 1;
}
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: 'Missile',
desc: 'Bullets home in on enemies',
color: 0xFF77FF,
onSelect: function onSelect() {
Bullet.prototype.homing = true;
window._missileChosen = true;
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: typeof hero !== "undefined" && typeof hero._doubleShotCount === "number" ? 'Extra Shot +1' : 'Extra Shot',
desc: 'Adds additional shot',
color: 0xFFD700,
onSelect: function onSelect() {
if (typeof hero._doubleShotCount === "undefined") {
hero._doubleShotCount = 2;
hero._doubleShot = true;
} else {
hero._doubleShotCount += 1;
hero._doubleShot = true;
}
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}];
// --- Shuffle and pick 4 random options ---
function shuffleArray(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// Track if missile has been chosen
if (typeof window._missileChosen === "undefined") {
window._missileChosen = false;
}
var upgradeOptions = allUpgradeOptions.slice();
// Remove 'Missile' option if already chosen
if (window._missileChosen) {
upgradeOptions = upgradeOptions.filter(function (opt) {
return opt.label !== 'Missile';
});
}
shuffleArray(upgradeOptions);
var selectedOptions = upgradeOptions.slice(0, 4);
// --- Render 4 random options ---
for (var i = 0; i < selectedOptions.length; i++) {
var opt = selectedOptions[i];
var optContainer = new Container();
var optBg = LK.getAsset('Button', {
anchorX: 0.5,
anchorY: 0.5
});
optBg.width = 700;
optBg.height = 200;
optBg.alpha = 0.98;
optBg.x = centerX;
optBg.y = optionStartY + optionSpacing * i;
optContainer.addChild(optBg);
var optTxt = new Text2(opt.label, {
size: 54,
fill: opt.color,
font: "Montserrat"
});
optTxt.anchor.set(0.5, 0.5);
optTxt.x = centerX;
optTxt.y = optionStartY + optionSpacing * i - 25;
optContainer.addChild(optTxt);
var optDesc = new Text2(opt.desc, {
size: 40,
fill: "#fff",
font: "Montserrat"
});
optDesc.anchor.set(0.5, 0.5);
optDesc.x = centerX;
optDesc.y = optionStartY + optionSpacing * i + 35;
optContainer.addChild(optDesc);
optContainer.interactive = true;
// Defensive: wrap onSelect to also close popup and resume game
optContainer.down = function (opt) {
return function (x, y, obj) {
opt.onSelect();
if (startOptionPopup && startOptionPopup.parent) {
startOptionPopup.parent.removeChild(startOptionPopup);
startOptionPopup = null;
}
game._levelUpFrozen = false;
};
}(opt);
startOptionPopup.addChild(optContainer);
}
// Add popup to game
game.addChild(startOptionPopup);
}
// Show the popup at game start
hero = new Hero();
hero.x = centerX;
hero.y = centerY;
game.addChild(hero);
showStartOptionPopup();
// Removed scoreTxt, replaced by timer in top right
var enemyKillCount = 0;
var achievement50Shown = false;
var achievement50Timeout = null;
var achievement50Txt = new Text2('Achievement: 50 Kills!', {
size: 80,
fill: 0xFFD700,
font: "Montserrat"
});
achievement50Txt.anchor.set(0.5, 0);
achievement50Txt.x = 2048 / 2;
// achievement50Txt.y will be set after magnetTimerTxt is added and y is set
achievement50Txt.visible = false;
var enemyKillTxt = new Text2('Kills: 0', {
size: 54,
fill: 0xFFD700,
font: "Montserrat"
});
// Anchor bottom right
enemyKillTxt.anchor.set(1, 1);
// Place at bottom right corner, with 40px margin from right and bottom
enemyKillTxt.x = -40;
enemyKillTxt.y = -40;
LK.gui.bottomRight.addChild(enemyKillTxt);
game.addChild(achievement50Txt);
var livesTxt = new Text2('Lives: ' + heroLives, {
size: 54,
fill: 0xFF5555,
font: "Montserrat"
});
livesTxt.anchor.set(1, 0);
livesTxt.y = 10 + 60 - 10;
livesTxt.x = -25;
LK.gui.topRight.addChild(livesTxt);
var timerTxt = new Text2('00:00', {
size: 64,
fill: "#fff",
font: "Montserrat"
});
timerTxt.anchor.set(0.5, 0);
timerTxt.y = 55;
LK.gui.top.addChild(timerTxt);
var magnetTimerTxt = new Text2('', {
size: 44,
fill: 0x00FFFF,
font: "Montserrat"
});
magnetTimerTxt.anchor.set(0.5, 0);
var attackSpeedTimerTxt = new Text2('', {
size: 44,
fill: 0xFFD700,
font: "Montserrat"
});
attackSpeedTimerTxt.anchor.set(0.5, 0);
xpTxt = new Text2('XP: 0/10', {
size: 44,
fill: 0x7BE495,
font: "Montserrat"
});
xpTxt.anchor.set(1, 0);
// Move XP text below the lives text (livesTxt.y + livesTxt.height + 8 for spacing), then 30px further down, then 70px further down, then 20px further down, then 10px up, then 60px up
xpTxt.y = livesTxt.y + livesTxt.height + 8 - 30 + 30 + 70 + 20 - 10 - 60;
xpTxt.x = -25 - 25 + 5;
LK.gui.topRight.addChild(xpTxt);
levelTxt = new Text2('Level: 1', {
size: 44,
fill: 0xF7E967,
font: "Montserrat"
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
levelTxt.y = 150;
// Now that levelTxt is defined, set magnetTimerTxt.y and add to LK.gui.top
magnetTimerTxt.y = levelTxt.y + levelTxt.height + 10;
LK.gui.top.addChild(magnetTimerTxt);
// Set attackSpeedTimerTxt.y below magnetTimerTxt and add to LK.gui.top
attackSpeedTimerTxt.y = magnetTimerTxt.y + magnetTimerTxt.height + 10;
LK.gui.top.addChild(attackSpeedTimerTxt);
// Now that magnetTimerTxt and attackSpeedTimerTxt are added and y is set, set achievement50Txt.y
achievement50Txt.y = attackSpeedTimerTxt.y + attackSpeedTimerTxt.height + 20;
function spawnEnemy() {
var edge = Math.floor(Math.random() * 4);
var x, y;
var useRight = false;
if (edge === 0) {
x = Math.random() * 2048;
y = -100;
if (x > 1024) {
useRight = true;
}
} else if (edge === 1) {
x = 2048 + 100;
y = Math.random() * 2732;
useRight = true;
} else if (edge === 2) {
x = Math.random() * 2048;
y = 2732 + 100;
if (x > 1024) {
useRight = true;
}
} else {
x = -100;
y = Math.random() * 2732;
}
// 10% chance to make this enemy a rare fast enemy (speed 5, not a boss/final boss)
var isFast = false;
if (Math.random() < 0.10) {
isFast = true;
}
// Every 5th enemy, spawn a RangedEnemy instead
var enemy;
if (typeof window._rangedEnemyCounter === "undefined") {
window._rangedEnemyCounter = 0;
}
window._rangedEnemyCounter++;
if (window._rangedEnemyCounter % 5 === 0) {
enemy = new RangedEnemy();
} else {
enemy = new Enemy({
fastEnemy: isFast
});
}
// No need to set speed here, handled in Enemy class
// Set initial facing direction based on spawn side
if (useRight) {
// Face right: right sprite visible, left sprite hidden
if (enemy.children && enemy.children.length > 0) {
for (var i = 0; i < enemy.children.length; i++) {
var child = enemy.children[i];
if (child.assetId === 'enemy_right' || child.assetId === 'zombiedog_right') {
child.visible = true;
}
if (child.assetId === 'enemy_left' || child.assetId === 'zombiedog_left') {
child.visible = false;
}
}
}
enemy._lastFacingRight = true;
} else {
// Face left: left sprite visible, right sprite hidden
if (enemy.children && enemy.children.length > 0) {
for (var i = 0; i < enemy.children.length; i++) {
var child = enemy.children[i];
if (child.assetId === 'enemy_right' || child.assetId === 'zombiedog_right') {
child.visible = false;
}
if (child.assetId === 'enemy_left' || child.assetId === 'zombiedog_left') {
child.visible = true;
}
}
}
enemy._lastFacingRight = false;
}
enemy.x = x;
enemy.y = y;
enemies.push(enemy);
game.addChild(enemy);
}
function spawnGem(x, y, opts) {
var gem = new Gem(opts);
gem.x = x;
gem.y = y;
gems.push(gem);
game.addChild(gem);
}
function spawnMagnetPowerup(x, y) {
var powerup = new Powerup({
isMagnet: true
}); // isMagnet parametresiyle oluştur
powerup.x = x;
powerup.y = y;
powerups.push(powerup);
game.addChild(powerup);
}
function spawnPowerup(x, y) {
var powerup = new Powerup({
isMagnet: false
}); // Diğer poweruplar
powerup.x = x;
powerup.y = y;
powerups.push(powerup);
// Remove and re-add to ensure it's above any background (if present)
if (game.children && game.children.length > 0) {
game.removeChild(powerup);
game.addChild(powerup);
} else {
game.addChild(powerup);
}
}
// Yeni: kalp powerup spawn fonksiyonu
function spawnHeartPowerup(x, y) {
var heart = new HeartPowerup();
heart.x = x;
heart.y = y;
powerups.push(heart);
game.addChild(heart);
}
// Spawn attack speed powerup
function spawnAttackSpeedPowerup(x, y) {
var asp = new AttackSpeedPowerup();
asp.x = x;
asp.y = y;
powerups.push(asp);
game.addChild(asp);
}
function fireBullet(dx, dy) {
var bullet = new Bullet();
bullet.x = hero.x;
bullet.y = hero.y;
if (typeof Bullet.prototype.pierce === "undefined") {
Bullet.prototype.pierce = 1;
}
bullet.pierce = Bullet.prototype.pierce;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
bullet.dirX = 1;
bullet.dirY = 0;
} else {
bullet.dirX = dx / dist;
bullet.dirY = dy / dist;
}
bullet.rotation = Math.atan2(bullet.dirY, bullet.dirX);
bullets.push(bullet);
game.addChild(bullet);
}
function randomDir() {
var angle = Math.random() * Math.PI * 2;
return {
x: Math.cos(angle),
y: Math.sin(angle)
};
}
function dist2(a, b) {
// 1) Mutlaka a ve b tanımlı olmalı
if (!a || !b) {
return 99999;
}
// 2) x,y özellikleri sayı değilse
if (typeof a.x !== "number" || typeof a.y !== "number" || typeof b.x !== "number" || typeof b.y !== "number") {
return 99999;
}
// 3) radius yoksa width/height'den ya da 0'dan tahmin et
var aRadius = 0;
if (a && typeof a.radius === "number") {
aRadius = a.radius;
} else if (a && typeof a.width === "number" && typeof a.height === "number") {
aRadius = Math.max(a.width, a.height) / 2;
} else {
aRadius = 0;
}
var bRadius = 0;
if (b && typeof b.radius === "number") {
bRadius = b.radius;
} else if (b && typeof b.width === "number" && typeof b.height === "number") {
bRadius = Math.max(b.width, b.height) / 2;
} else {
bRadius = 0;
}
// 4) İki nokta arası mesafe
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
game.down = function (x, y, obj) {
if (game._levelUpFrozen) {
dragging = false;
return;
}
// Do not pause the game on left click/tap; just move the hero if not in the top-left menu area
if (x < 100 && y < 100) {
return;
}
// Prevent updating targetX/targetY if any popup is open (startOptionPopup or levelUpPopup)
if (typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent) {
return;
}
hero.targetX = x;
hero.targetY = y;
dragging = true;
};
game.move = function (x, y, obj) {
if (game._levelUpFrozen) {
return;
}
// Prevent updating targetX/targetY if any popup is open (startOptionPopup or levelUpPopup)
if (typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent) {
return;
}
hero.targetX = x;
hero.targetY = y;
};
game.up = function (x, y, obj) {
if (game._levelUpFrozen) {
return;
}
dragging = false;
};
var ticksSurvived = 0;
var autoAttackTimer = 0;
var autoAttackInterval = 96;
game.update = function () {
// --- Final Boss logic at 5 minutes ---
if (typeof finalBossActive === "undefined") {
finalBossActive = false;
}
if (typeof finalBossDefeated === "undefined") {
finalBossDefeated = false;
}
if (typeof finalBoss === "undefined") {
finalBoss = null;
}
if (typeof finalBossTriggered === "undefined") {
finalBossTriggered = false;
}
if (typeof ticksSurvived !== "undefined" && !finalBossTriggered && Math.floor(ticksSurvived / 60) >= 120) {
// 2 minutes reached, trigger Final Boss
finalBossTriggered = true;
finalBossActive = true;
finalBossDefeated = false;
// Destroy all enemies on screen
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] && enemies[i].parent) {
enemies[i].destroy();
}
enemies.splice(i, 1);
}
// Spawn Final Boss at top center (x = centerX, y = -200)
finalBoss = new Enemy({
isBoss: true,
isFinalBoss: true // use special asset
});
finalBoss.x = centerX;
finalBoss.y = -200;
finalBoss.hp = 30 * 10; // 10x normal boss HP
enemies.push(finalBoss);
game.addChild(finalBoss);
}
// If Final Boss is active, halt wave spawns
if (finalBossActive && finalBoss && typeof finalBoss.hp === "number" && finalBoss.hp > 0) {
// Only update Final Boss and other logic, skip wave/enemy spawns
// Allow all other update logic to run, but skip spawnTimer/wave logic below
// Check if Final Boss is defeated
if (finalBoss.hp <= 0) {
finalBossActive = false;
finalBossDefeated = true;
finalBoss = null;
// Resume normal wave spawning next frame
}
} else if (finalBossActive && (!finalBoss || finalBoss.hp <= 0)) {
// Final Boss defeated, resume normal wave spawning
finalBossActive = false;
finalBossDefeated = true;
finalBoss = null;
}
if (game._levelUpFrozen) {
// Freeze all game logic and input while level up popup is active
return;
}
// --- Invulnerability circle effect logic ---
if (typeof hero !== "undefined") {
// Track invuln circle instance globally
if (typeof invulnCircle === "undefined") {
invulnCircle = null;
}
var invulnActive = typeof hero._invulnerableUntilTick === "number" && typeof ticksSurvived === "number" && ticksSurvived < hero._invulnerableUntilTick;
if (invulnActive) {
if (!invulnCircle || !invulnCircle.parent) {
invulnCircle = new InvulnerabilityCircle();
invulnCircle.x = hero.x;
invulnCircle.y = hero.y;
// Add above hero in display list
var heroIdx = game.children.indexOf(hero);
if (heroIdx !== -1) {
game.addChildAt(invulnCircle, heroIdx + 1);
} else {
game.addChild(invulnCircle);
}
}
// Always ensure hero is behind invulnCircle in display list
if (invulnCircle && invulnCircle.parent && hero && hero.parent === game) {
var invulnIdx = game.children.indexOf(invulnCircle);
var heroIdx = game.children.indexOf(hero);
if (heroIdx > invulnIdx - 1) {
// Remove and re-add hero just before invulnCircle
game.removeChild(hero);
game.addChildAt(hero, invulnIdx);
}
}
} else {
if (invulnCircle && invulnCircle.parent) {
invulnCircle.parent.removeChild(invulnCircle);
invulnCircle = null;
}
}
if (invulnCircle) {
invulnCircle.update();
}
// --- Magnet circle effect logic ---
if (typeof magnetCircle === "undefined") {
magnetCircle = null;
}
var magnetActive = hero.magnetActive && hero.magnetDuration > 0;
if (magnetActive) {
if (!magnetCircle || !magnetCircle.parent) {
magnetCircle = new MagnetCircle();
magnetCircle.x = hero.x;
magnetCircle.y = hero.y;
// Add below hero in display list
var heroIdx = game.children.indexOf(hero);
if (heroIdx !== -1) {
game.addChildAt(magnetCircle, heroIdx);
} else {
game.addChild(magnetCircle);
}
}
} else {
if (magnetCircle && magnetCircle.parent) {
magnetCircle.parent.removeChild(magnetCircle);
magnetCircle = null;
}
}
if (magnetCircle) {
magnetCircle.update();
}
}
hero.update();
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
e.update();
// Track lastWasTouchingHero for exact frame detection
if (typeof e.lastWasTouchingHero === "undefined") {
e.lastWasTouchingHero = false;
}
var isTouchingHero = dist2(e, hero) < e.radius + hero.radius;
if (!e.lastWasTouchingHero && isTouchingHero) {
// Check for hero invulnerability
if (typeof hero._invulnerableUntilTick === "number" && typeof ticksSurvived === "number" && ticksSurvived < hero._invulnerableUntilTick) {
// Do nothing, hero is invulnerable
} else {
LK.effects.flashScreen(0xff0000, 1000);
heroLives--;
livesTxt.setText('Lives: ' + heroLives);
if (heroLives <= 0) {
LK.showGameOver();
lastGameOver = true;
}
}
}
e.lastWasTouchingHero = isTouchingHero;
}
// --- RangedEnemy projectile update and collision ---
for (var i = enemyProjectiles.length - 1; i >= 0; i--) {
var proj = enemyProjectiles[i];
proj.update();
// Remove if offscreen
if (proj.x < -100 || proj.x > 2148 || proj.y < -100 || proj.y > 2832) {
proj.destroy();
enemyProjectiles.splice(i, 1);
continue;
}
// Collision with hero
if (dist2(proj, hero) < proj.radius + hero.radius) {
// Check for hero invulnerability
if (!(typeof hero._invulnerableUntilTick === "number" && typeof ticksSurvived === "number" && ticksSurvived < hero._invulnerableUntilTick)) {
LK.effects.flashScreen(0xff0000, 1000);
heroLives--;
livesTxt.setText('Lives: ' + heroLives);
if (heroLives <= 0) {
LK.showGameOver();
lastGameOver = true;
}
}
proj.destroy();
enemyProjectiles.splice(i, 1);
continue;
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
var b = bullets[i];
b.update();
// Remove bullet if it goes off-screen (outside visible area)
if (b.x < -100 || b.x > 2148 || b.y < -100 || b.y > 2832) {
b.destroy();
bullets.splice(i, 1);
continue;
}
for (var j = enemies.length - 1; j >= 0; j--) {
var e = enemies[j];
if (b && typeof b.radius === "number" && e && typeof e.radius === "number" && dist2(b, e) < b.radius + e.radius) {
// Mark this enemy as hit by this bullet
if (b._hitEnemies && b._hitEnemies.indexOf(e) === -1) {
b._hitEnemies.push(e);
}
// Boss and normal enemy logic (bullets can pierce all enemy types)
if (typeof e.hp === "number" && e.hp > 0) {
e.hp -= 1;
LK.effects.flashObject(e, 0xffffff, 120);
if (e.hp <= 0) {
// --- Powerup drop logic for boss ---
var droppedPowerup = false;
// Restore: Boss/final boss powerup drop rates are fixed by type, not HP
var bossPowerupRate = e.isFinalBoss ? 0.5 : 0.25;
if (Math.random() < bossPowerupRate) {
// Pick one of the three powerups at random
var powerupType = Math.floor(Math.random() * 3);
if (powerupType === 0) {
spawnMagnetPowerup(e.x, e.y);
} else if (powerupType === 1) {
spawnHeartPowerup(e.x, e.y);
} else {
spawnAttackSpeedPowerup(e.x, e.y);
}
droppedPowerup = true;
}
// Boss defeated: drop multiple big gems and normal gems ONLY if no powerup dropped
if (!droppedPowerup) {
// Restore: Boss always drops 1 big xp gem and 5 normal gems (not based on HP)
var angle = Math.random() * Math.PI * 2;
var dist = 60 + Math.random() * 40;
var gemX = e.x + Math.cos(angle) * dist;
var gemY = e.y + Math.sin(angle) * dist;
spawnGem(gemX, gemY, {
type: 'big'
});
for (var drop = 0; drop < 5; drop++) {
var angle = Math.random() * Math.PI * 2;
var dist = 80 + Math.random() * 60;
var gemX = e.x + Math.cos(angle) * dist;
var gemY = e.y + Math.sin(angle) * dist;
spawnGem(gemX, gemY);
}
}
enemyKillCount++;
e.destroy();
// Remove Final Boss reference if this was the Final Boss
if (e.isFinalBoss) {
finalBossActive = false;
finalBossDefeated = true;
finalBoss = null;
}
enemies.splice(j, 1);
}
} else {
// --- Powerup drop logic for normal, fast, and ranged enemies ---
// Restore: Drop rates are based only on enemy type, not HP
var droppedPowerup = false;
// 2% chance to drop magnet powerup
if (Math.random() < 0.02) {
spawnMagnetPowerup(e.x, e.y);
droppedPowerup = true;
}
// 2% chance to drop heart powerup
if (Math.random() < 0.02) {
spawnHeartPowerup(e.x, e.y);
droppedPowerup = true;
}
// 2% chance to drop attack speed powerup
if (Math.random() < 0.02) {
spawnAttackSpeedPowerup(e.x, e.y);
droppedPowerup = true;
}
// Only drop XP gems if no powerup dropped
if (!droppedPowerup) {
var bigGemSpawned = false;
if (Math.random() < 0.10) {
// Drop big xp gem at 10% rate
spawnGem(e.x, e.y, {
type: 'big'
});
bigGemSpawned = true;
}
// Offset the normal gem if big gem was spawned to avoid overlap
if (bigGemSpawned) {
var offsetAngle = Math.random() * Math.PI * 2;
var offsetDist = 40;
var gemX = e.x + Math.cos(offsetAngle) * offsetDist;
var gemY = e.y + Math.sin(offsetAngle) * offsetDist;
spawnGem(gemX, gemY);
} else {
spawnGem(e.x, e.y);
}
}
enemyKillCount++;
e.destroy();
enemies.splice(j, 1);
}
// Always decrement pierce on every enemy hit, regardless of enemy type (including bosses/final bosses)
b.pierce -= 1;
// Ricochet logic: if bullet has ricochetLeft, bounce to nearest enemy
if (typeof b.ricochetLeft !== "undefined" && b.ricochetLeft > 0 && enemies.length > 1) {
// Find nearest enemy that is not the one just hit
var minDist = 99999;
var nearest = null;
for (var ricI = 0; ricI < enemies.length; ricI++) {
var ricE = enemies[ricI];
// Exclude the just-hit enemy and any already-hit enemies
if (ricE === e) {
continue;
}
if (b._hitEnemies && b._hitEnemies.indexOf(ricE) !== -1) {
continue;
}
var d = dist2(b, ricE);
if (d < minDist) {
minDist = d;
nearest = ricE;
}
}
if (nearest) {
// Ricochet: set bullet position to current, aim at nearest enemy
var dx = nearest.x - b.x;
var dy = nearest.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
b.dirX = dx / dist;
b.dirY = dy / dist;
b.rotation = Math.atan2(b.dirY, b.dirX);
b.ricochetLeft--;
// Don't destroy or remove bullet, let it continue
// Restore pierce for next hit
b.pierce = Math.max(1, Bullet.prototype.pierce || 1);
// Move bullet slightly toward new direction to avoid instant re-collision
b.x += b.dirX * 10;
b.y += b.dirY * 10;
continue;
}
}
}
// If no ricochet, destroy as normal, but only if pierce <= 0
if (b.pierce <= 0) {
b.destroy();
bullets.splice(i, 1);
}
break;
}
}
}
var _loop = function _loop() {
g = gems[i]; // Set XP gem attraction area to 800 pixels when magnet is active, otherwise 150
xpAttractRange = hero.magnetActive ? 800 : 150;
d = dist2(g, hero);
if (d < xpAttractRange) {
dx = hero.x - g.x;
dy = hero.y - g.y;
dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
g.x += dx / dist * 18;
g.y += dy / dist * 18;
}
}
if (g && typeof g.radius === "number" && hero && typeof hero.radius === "number" && dist2(g, hero) < g.radius + hero.radius) {
// Big XP gem gives 5 XP, normal gives 1
if (g.type === 'big') {
xp += 5;
} else {
xp += 1;
}
g.destroy();
gems.splice(i, 1);
if (xp >= xpToLevel) {
// --- Shuffle and pick 4 random options ---
var shuffleArray = function shuffleArray(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
};
level += 1;
xp = 0;
// Small automatic attack speed boost on every level up (less than upgrade option)
autoAttackInterval = Math.max(6, autoAttackInterval - 3);
// Easier XP curve: gentler growth for faster level up
xpToLevel = 8 + level * 5 + Math.floor(level * level * 0.7);
LK.effects.flashObject(hero, 0xf7e967, 600);
// On level up, spawn a big XP gem near hero
spawnGem(hero.x + (Math.random() - 0.5) * 200, hero.y + (Math.random() - 0.5) * 200, {
type: 'big'
});
// Pause game and show level up popup with two options
// Remove any previous popup if present
if (typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent) {
levelUpPopup.parent.removeChild(levelUpPopup);
levelUpPopup = null;
}
// Freeze all game logic and input
game._levelUpFrozen = true;
// Create popup container
levelUpPopup = new Container(); // Dim background (match start popup)
bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.57,
color: 0x008000 // green
});
bg.width = 1100;
bg.height = 1500;
bg.alpha = 0.92;
bg.x = centerX;
bg.y = centerY;
levelUpPopup.addChild(bg);
// Title replaced with LeveledUp asset
var leveledUpImg = LK.getAsset('LeveledUp', {
anchorX: 0.5,
anchorY: 0.25,
x: centerX,
y: centerY - 610
});
levelUpPopup.addChild(leveledUpImg);
// Option vertical layout (match start popup)
optionStartY = centerY - 320;
optionSpacing = 220; // --- Upgrade Option Definitions (same as start popup, but update text for in-game context) ---
allUpgradeOptions = [{
label: 'Attack Speed',
desc: 'Fire faster every level!',
color: 0xF7E967,
onSelect: function onSelect() {
autoAttackInterval = Math.max(6, autoAttackInterval - 12);
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: typeof Bullet.prototype.ricochet !== "undefined" && Bullet.prototype.ricochet >= 1 ? 'Ricochet +1 bounce' : 'Ricochet',
desc: 'Bullets bounces',
color: 0x7BE495,
onSelect: function onSelect() {
if (typeof Bullet.prototype.ricochet === "undefined" || Bullet.prototype.ricochet < 1) {
Bullet.prototype.ricochet = 1;
} else {
Bullet.prototype.ricochet += 1;
}
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: 'Bullet +1',
desc: 'Shoot an extra bullet',
color: 0xFFB347,
onSelect: function onSelect() {
if (typeof Bullet.prototype.extraBullets === "undefined") {
Bullet.prototype.extraBullets = 1;
} else {
Bullet.prototype.extraBullets += 1;
}
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: 'Pierce +1',
desc: 'Bullets pierce enemies',
color: 0x7BE4FF,
onSelect: function onSelect() {
if (typeof Bullet.prototype.pierce === "undefined") {
Bullet.prototype.pierce = 2;
} else {
Bullet.prototype.pierce += 1;
}
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: 'Missile',
desc: 'Bullets home in on enemies',
color: 0xFF77FF,
onSelect: function onSelect() {
Bullet.prototype.homing = true;
window._missileChosen = true;
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}, {
label: typeof hero !== "undefined" && typeof hero._doubleShotCount === "number" ? 'Extra Shot +1' : 'Extra Shot',
desc: 'Adds additional shot',
color: 0xFFD700,
onSelect: function onSelect() {
if (typeof hero._doubleShotCount === "undefined") {
hero._doubleShotCount = 2;
hero._doubleShot = true;
} else {
hero._doubleShotCount += 1;
hero._doubleShot = true;
}
hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180;
LK.effects.flashObject(hero, 0xffff00, 3000);
}
}];
// Track if missile has been chosen
if (typeof window._missileChosen === "undefined") {
window._missileChosen = false;
}
upgradeOptions = allUpgradeOptions.slice();
// Remove 'Missile' option if already chosen
if (window._missileChosen) {
upgradeOptions = upgradeOptions.filter(function (opt) {
return opt.label !== 'Missile';
});
}
shuffleArray(upgradeOptions);
selectedOptions = upgradeOptions.slice(0, 4); // --- Render 4 random options with Button asset background ---
for (i = 0; i < selectedOptions.length; i++) {
opt = selectedOptions[i];
optContainer = new Container();
optBg = LK.getAsset('Button', {
anchorX: 0.5,
anchorY: 0.5
});
optBg.width = 700;
optBg.height = 200;
optBg.alpha = 0.98;
optBg.x = centerX;
optBg.y = optionStartY + optionSpacing * i;
optContainer.addChild(optBg);
optTxt = new Text2(opt.label, {
size: 54,
fill: opt.color,
font: "Montserrat"
});
optTxt.anchor.set(0.5, 0.5);
optTxt.x = centerX;
optTxt.y = optionStartY + optionSpacing * i - 25;
optContainer.addChild(optTxt);
optDesc = new Text2(opt.desc, {
size: 40,
fill: "#fff",
font: "Montserrat"
});
optDesc.anchor.set(0.5, 0.5);
optDesc.x = centerX;
optDesc.y = optionStartY + optionSpacing * i + 35;
optContainer.addChild(optDesc);
optContainer.interactive = true;
optContainer.down = function (opt) {
return function (x, y, obj) {
opt.onSelect();
if (levelUpPopup && levelUpPopup.parent) {
levelUpPopup.parent.removeChild(levelUpPopup);
levelUpPopup = null;
}
game._levelUpFrozen = false;
};
}(opt);
levelUpPopup.addChild(optContainer);
}
// Add popup to game
game.addChild(levelUpPopup);
}
}
},
g,
xpAttractRange,
d,
dx,
dy,
dist,
levelUpPopup,
bg,
titleTxt,
optionStartY,
optionSpacing,
allUpgradeOptions,
upgradeOptions,
selectedOptions,
i,
opt,
optContainer,
optBg,
optTxt,
optDesc;
for (var i = gems.length - 1; i >= 0; i--) {
_loop();
}
for (var i = powerups.length - 1; i >= 0; i--) {
var p = powerups[i];
// Powerup collection area: always 150px (not affected by magnet)
var powerupAttractRange = 150;
var d = dist2(p, hero);
// Attract powerups (magnet, heart, and others) if within hero's collection area (150px), but NOT by magnet effect
if (d < powerupAttractRange && d > p.radius + hero.radius) {
// Move powerup toward hero (gentle attraction)
var dx = hero.x - p.x;
var dy = hero.y - p.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
p.x += dx / dist * 10;
p.y += dy / dist * 10;
}
}
// Magnet powerup pickup radius (same as attract range)
if (p.isMagnet && d < p.radius + hero.radius) {
hero.magnetActive = true;
// Set magnetEndTick to 10 seconds (600 frames) from now, synchronized with game time
if (typeof ticksSurvived === "number") {
hero.magnetEndTick = ticksSurvived + 600;
hero.magnetDuration = 600;
} else {
hero.magnetEndTick = undefined;
hero.magnetDuration = 600;
}
hero._magnetGlowActive = false; // force re-apply blue glow in Hero.update
LK.effects.flashObject(hero, 0x00ffff, 800);
p.destroy();
powerups.splice(i, 1);
}
// Yeni: Kalp powerup toplama
else if (p.constructor === HeartPowerup && d < p.radius + hero.radius) {
heroLives++;
if (heroLives > 5) {
heroLives = 5;
}
livesTxt.setText('Lives: ' + heroLives);
p.destroy();
powerups.splice(i, 1);
}
// Diğer poweruplar için (şu an sadece magnet ve heart var)
else if (!p.isMagnet && !(p.constructor === HeartPowerup) && d < p.radius + hero.radius) {
// Attack speed powerup pickup
if (p.constructor === AttackSpeedPowerup) {
// Set attack speed boost for 5 seconds (300 ticks), but do not stack duration, just reset to 5s
hero._attackSpeedBoostEndTick = ticksSurvived + 300;
LK.effects.flashObject(hero, 0xffff00, 800);
}
// Pick up other powerups if needed (future-proof)
p.destroy();
powerups.splice(i, 1);
}
}
// Handle attack speed boost effect
if (typeof hero._attackSpeedBoostEndTick !== "undefined" && typeof ticksSurvived !== "undefined" && ticksSurvived < hero._attackSpeedBoostEndTick) {
if (typeof hero._attackSpeedBoostActive === "undefined" || !hero._attackSpeedBoostActive) {
hero._attackSpeedBoostActive = true;
hero._originalAutoAttackInterval = autoAttackInterval;
autoAttackInterval = Math.max(3, Math.floor(autoAttackInterval / 2));
}
} else if (typeof hero._attackSpeedBoostActive !== "undefined" && hero._attackSpeedBoostActive) {
hero._attackSpeedBoostActive = false;
if (typeof hero._originalAutoAttackInterval !== "undefined") {
autoAttackInterval = hero._originalAutoAttackInterval;
}
}
autoAttackTimer++;
if (autoAttackTimer >= autoAttackInterval) {
autoAttackTimer = 0;
// Block firing if reloading
if (heroReloading) {
// Do nothing, wait for reload to finish
} else {
var nearest = null,
minDist = 99999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var d = dist2(hero, e);
if (d < minDist) {
minDist = d;
nearest = e;
}
}
if (nearest) {
var dx = nearest.x - hero.x;
var dy = nearest.y - hero.y;
// Remove automatic extra bullets from level; now controlled by upgrade
if (typeof Bullet.prototype.extraBullets === "undefined") {
Bullet.prototype.extraBullets = 0;
}
var extraBullets = Bullet.prototype.extraBullets;
var totalBullets = 1 + extraBullets;
var spread;
var baseAngle = Math.atan2(dy, dx);
// Double Shot logic: if enabled, fire multiple volleys in succession (not at the same time)
var doubleShotActive = hero._doubleShot === true;
var doubleShotCount = typeof hero._doubleShotCount === "number" && hero._doubleShotCount > 1 ? hero._doubleShotCount : doubleShotActive ? 2 : 1;
// Count how many shots will be fired (1 volley or more for double shot)
var shotsToFire = doubleShotActive ? doubleShotCount : 1;
// If not enough bullets left, only fire as many as available
if (heroBulletCount < shotsToFire) {
shotsToFire = heroBulletCount;
}
// If no bullets left, start reload and block firing
if (heroBulletCount <= 0) {
if (!heroReloading) {
heroReloading = true;
reloadTxt.setText('Reloading...');
heroReloadTimeout = LK.setTimeout(function () {
heroBulletCount = heroBulletMax;
heroReloading = false;
reloadTxt.setText('');
}, 1000);
}
} else {
// Actually fire
if (doubleShotActive) {
// Fire multiple volleys in succession, number of volleys = shotsToFire
(function fireDoubleShotVolley(volleyIdx) {
if (heroBulletCount <= 0 || volleyIdx >= shotsToFire) {
return;
}
var shotAngle = baseAngle;
// Double shot bullets go in the same direction (no angle offset between volleys)
var doubleShotAngleOffset = 0;
shotAngle = baseAngle;
if (totalBullets % 2 === 0 && totalBullets > 1) {
// Even number of bullets: 2 center bullets go straight and parallel, others scatter
var centerIdx1 = totalBullets / 2 - 1;
var centerIdx2 = totalBullets / 2;
var offsetDist = 30;
var perpAngle = shotAngle + Math.PI / 2;
for (var b = 0; b < totalBullets; b++) {
if (b === centerIdx1 || b === centerIdx2) {
var offset = (b === centerIdx1 ? -1 : 1) * offsetDist / 2;
var bulletX = hero.x + Math.cos(perpAngle) * offset;
var bulletY = hero.y + Math.sin(perpAngle) * offset;
var dirX = Math.cos(shotAngle);
var dirY = Math.sin(shotAngle);
var bullet = new Bullet();
bullet.x = bulletX;
bullet.y = bulletY;
bullet.pierce = Bullet.prototype.pierce || 1;
bullet.dirX = dirX;
bullet.dirY = dirY;
bullet.rotation = shotAngle;
bullets.push(bullet);
game.addChild(bullet);
} else {
// Scattered bullets
var scatterCount = totalBullets - 2;
var scatterIdx = b < centerIdx1 ? b : b - 2;
var scatterSpread = Math.PI / 32 + (level - 2) * Math.PI / 48;
if (scatterSpread > Math.PI / 4) {
scatterSpread = Math.PI / 4;
}
var angle = shotAngle;
if (scatterCount > 1) {
angle = shotAngle - scatterSpread / 2 + scatterSpread * scatterIdx / (scatterCount - 1);
}
var dirX = Math.cos(angle);
var dirY = Math.sin(angle);
fireBullet(dirX, dirY);
}
}
} else if (level > 2) {
var spread = Math.PI / 32 + (level - 2) * Math.PI / 48;
if (spread > Math.PI / 4) {
spread = Math.PI / 4;
}
for (var b = 0; b < totalBullets; b++) {
var angle = shotAngle;
if (totalBullets > 1) {
angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1);
}
var dirX = Math.cos(angle);
var dirY = Math.sin(angle);
fireBullet(dirX, dirY);
}
} else {
var spread = Math.PI / 16;
for (var b = 0; b < totalBullets; b++) {
var angle = shotAngle;
if (totalBullets > 1) {
angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1);
}
var dirX = Math.cos(angle);
var dirY = Math.sin(angle);
fireBullet(dirX, dirY);
}
}
heroBulletCount--;
reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax);
// If out of ammo after this shot, trigger reload
if (heroBulletCount <= 0 && !heroReloading) {
heroReloading = true;
reloadTxt.setText('Reloading...');
heroReloadTimeout = LK.setTimeout(function () {
heroBulletCount = heroBulletMax;
heroReloading = false;
reloadTxt.setText('');
}, 1000);
}
// Schedule next volley if more remain
if (volleyIdx + 1 < shotsToFire && heroBulletCount > 0) {
LK.setTimeout(function () {
fireDoubleShotVolley(volleyIdx + 1);
}, 300); // 300ms delay between shots
}
})(0);
} else {
// Single volley (normal fire)
var shotAngle = baseAngle;
if (totalBullets % 2 === 0 && totalBullets > 1) {
var centerIdx1 = totalBullets / 2 - 1;
var centerIdx2 = totalBullets / 2;
var offsetDist = 30;
var perpAngle = shotAngle + Math.PI / 2;
for (var b = 0; b < totalBullets; b++) {
if (b === centerIdx1 || b === centerIdx2) {
var offset = (b === centerIdx1 ? -1 : 1) * offsetDist / 2;
var bulletX = hero.x + Math.cos(perpAngle) * offset;
var bulletY = hero.y + Math.sin(perpAngle) * offset;
var dirX = Math.cos(shotAngle);
var dirY = Math.sin(shotAngle);
var bullet = new Bullet();
bullet.x = bulletX;
bullet.y = bulletY;
bullet.pierce = Bullet.prototype.pierce || 1;
bullet.dirX = dirX;
bullet.dirY = dirY;
bullet.rotation = shotAngle;
bullets.push(bullet);
game.addChild(bullet);
} else {
var scatterCount = totalBullets - 2;
var scatterIdx = b < centerIdx1 ? b : b - 2;
var scatterSpread = Math.PI / 32 + (level - 2) * Math.PI / 48;
if (scatterSpread > Math.PI / 4) {
scatterSpread = Math.PI / 4;
}
var angle = shotAngle;
if (scatterCount > 1) {
angle = shotAngle - scatterSpread / 2 + scatterSpread * scatterIdx / (scatterCount - 1);
}
var dirX = Math.cos(angle);
var dirY = Math.sin(angle);
fireBullet(dirX, dirY);
}
}
} else if (level > 2) {
var spread = Math.PI / 32 + (level - 2) * Math.PI / 48;
if (spread > Math.PI / 4) {
spread = Math.PI / 4;
}
for (var b = 0; b < totalBullets; b++) {
var angle = shotAngle;
if (totalBullets > 1) {
angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1);
}
var dirX = Math.cos(angle);
var dirY = Math.sin(angle);
fireBullet(dirX, dirY);
}
} else {
var spread = Math.PI / 16;
for (var b = 0; b < totalBullets; b++) {
var angle = shotAngle;
if (totalBullets > 1) {
angle = shotAngle - spread / 2 + spread * b / (totalBullets - 1);
}
var dirX = Math.cos(angle);
var dirY = Math.sin(angle);
fireBullet(dirX, dirY);
}
}
heroBulletCount--;
reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax);
if (heroBulletCount <= 0 && !heroReloading) {
heroReloading = true;
reloadTxt.setText('Reloading...');
heroReloadTimeout = LK.setTimeout(function () {
heroBulletCount = heroBulletMax;
heroReloading = false;
reloadTxt.setText('');
}, 1000);
}
}
}
}
}
}
// Prevent enemy/wave spawn logic while Final Boss is active
if (!finalBossActive) {
spawnTimer++;
if (spawnTimer >= spawnInterval) {
spawnTimer = 0;
var toSpawn = Math.max(1, Math.floor((1 + Math.floor(wave / 3)) / 4 * 0.75));
for (var i = 0; i < toSpawn; i++) {
spawnEnemy();
}
// Boss spawn logic: spawn boss every 30 waves
if (typeof bossSpawnedWaves === "undefined") {
bossSpawnedWaves = {};
}
if (wave % 30 === 0 && !bossSpawnedWaves[wave]) {
var boss = new Enemy({
isBoss: true
});
// Spawn boss at a random edge
var edge = Math.floor(Math.random() * 4);
if (edge === 0) {
boss.x = Math.random() * 2048;
boss.y = -200;
} else if (edge === 1) {
boss.x = 2048 + 200;
boss.y = Math.random() * 2732;
} else if (edge === 2) {
boss.x = Math.random() * 2048;
boss.y = 2732 + 200;
} else {
boss.x = -200;
boss.y = Math.random() * 2732;
}
enemies.push(boss);
game.addChild(boss);
bossSpawnedWaves[wave] = true;
}
wave++;
spawnInterval = Math.max(24, 90 - Math.floor(wave / 2));
}
}
ticksSurvived++;
// Removed scoreTxt.setText, timer is shown in top right only
enemyKillTxt.setText('Kills: ' + enemyKillCount);
if (!achievement50Shown && enemyKillCount >= 50) {
achievement50Shown = true;
achievement50Txt.visible = true;
if (achievement50Timeout) {
LK.clearTimeout(achievement50Timeout);
}
achievement50Timeout = LK.setTimeout(function () {
achievement50Txt.visible = false;
}, 2000);
}
xpTxt.setText('XP: ' + xp + '/' + xpToLevel);
levelTxt.setText('Level: ' + level);
var totalSeconds = Math.floor(ticksSurvived / 60);
var minutes = Math.floor(totalSeconds / 60);
var seconds = totalSeconds % 60;
var minStr = minutes < 10 ? '0' + minutes : '' + minutes;
var secStr = seconds < 10 ? '0' + seconds : '' + seconds;
timerTxt.setText(minStr + ':' + secStr);
// Only reset heroLives and livesTxt on game restart, not every frame
if (lastGameOver && hero && hero.parent) {
lastGameOver = false;
heroLives = 5;
if (heroLives > 5) {
heroLives = 5;
}
if (livesTxt) {
livesTxt.setText('Lives: ' + heroLives);
}
// Reset bullet count and reload state
heroBulletCount = heroBulletMax;
heroReloading = false;
reloadTxt.setText('');
if (heroReloadTimeout) {
LK.clearTimeout(heroReloadTimeout);
heroReloadTimeout = null;
}
}
// Magnet/Attack Speed timer display
if (hero.magnetActive && hero.magnetDuration > 0) {
var magnetSeconds = Math.ceil(hero.magnetDuration / 60);
magnetTimerTxt.setText('Magnet: ' + magnetSeconds + 's');
} else {
magnetTimerTxt.setText('');
}
if (typeof hero._attackSpeedBoostEndTick !== "undefined" && typeof ticksSurvived !== "undefined" && ticksSurvived < hero._attackSpeedBoostEndTick) {
var aspSeconds = Math.ceil((hero._attackSpeedBoostEndTick - ticksSurvived) / 60);
attackSpeedTimerTxt.setText('Attack Speed: ' + aspSeconds + 's');
} else {
attackSpeedTimerTxt.setText('');
}
// Show ammo count if not reloading
if (!heroReloading) {
reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax);
}
};
16x16 pixel wounded guy holding pistol. In-Game asset. 2d. High contrast. No shadows. pixel art. retro arcade game
3x3 pixel green coin. In-Game asset. 2d. High contrast. No shadows. retro arcade. Pixel art
3x3 pixel blue coin. In-Game asset. 2d. High contrast. No shadows. Pixel art. retro arcade
4x4 pixel art heart. In-Game asset. 2d. High contrast. No shadows. retro arcade. Pixel art. 8 bit
fill the circle with yellow colour
remove cars and buildings
Create an 8-bit style effect representing a magnetic power-up area. The effect should be a circular, glowing field with a soft, pulsing light. The colors should be green and blue, with a slight gradient effect to indicate the area where objects (such as coins or experience points) are attracted towards the character. The circle should have a subtle flicker to show the magnetic pull, and it should be designed to fit within the retro, pixel-art aesthetic of an 8-bit game. In-Game asset. 2d. High contrast. No shadows
Vertical windowed filled rectangle HUD for the 2d zombie theme game. Use green colours. Do not make it too much pixelated. In-Game asset. 2d. High contrast. No shadows. No text. No icon. No background Transparent.Retro arcade theme.
windowed filled rectangle HUD button for the 2d pixel art zombie theme game. Use dark green colours. In-Game asset. 2d. High contrast. No shadows. No text. No icon. No background Transparent.Retro arcade theme.
pixelart magnet In-Game asset. 2d. High contrast. No shadows. Pixel art
pixelart red circular enemy projectile to dodge In-Game asset. 2d. High contrast. No shadows. Pixel art
pixelart yellow circular bullet to shoot enemies In-Game asset. 2d. High contrast. No shadows. Pixel art
pixelart blue circular enemy projectile to dodge In-Game asset. 2d. High contrast. No shadows. Pixel art
4x4pixel bow and arrow. In-Game asset. 2d. High contrast. No shadows. Black outline
8x8 pixel movement speed powerup icon. boot with wings. In-Game asset. 2d. High contrast. No shadows. Black outline