User prompt
remove missile active from the start
User prompt
it is not looping
User prompt
play lowhp sound asset continually when heros live is 1
User prompt
play low hp sound continually when heros live is 1
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting '_playing')' in or related to this line: 'window._lowHpSoundInstance._playing = true;' Line Number: 1767
User prompt
play low hp sound continually when heros live is 1
User prompt
play lowhp sound as a loop when heros remaining life is 1
User prompt
make 2 center bullet home paralelly when the bullet count is odd number by doing as // Çift sayıda homing mermi: ortadaki 2 mermi paralel, geri kalan scatter else if (Bullet.prototype.homing && totalBullets > 1 && totalBullets % 2 === 0) { var shotAngle = baseAngle; var perpAngle = shotAngle + Math.PI/2; var offsetDist = 30; // paralel mermiler arası mesafe // 1) Orta iki mermiyi paralel ateşle for (var c = 0; c < 2; c++) { var off = (c === 0 ? -offsetDist/2 : offsetDist/2); var bx = hero.x + Math.cos(perpAngle) * off; var by = hero.y + Math.sin(perpAngle) * off; var mb = new Bullet(); mb.x = bx; mb.y = by; mb.pierce = Bullet.prototype.pierce || 1; mb.dirX = Math.cos(shotAngle); mb.dirY = Math.sin(shotAngle); mb.rotation = shotAngle; bullets.push(mb); game.addChild(mb); } // 2) Kalan mermeleri scatter olarak yay var scatterCount = totalBullets - 2; if (scatterCount > 0) { var maxSpread = Math.PI/4; var baseSpread = Math.PI/32 + Math.max(0, level-2)*Math.PI/48; var spread = Math.min(baseSpread, maxSpread); for (var s = 0; s < scatterCount; s++) { var angle = shotAngle - spread/2 + (scatterCount > 1 ? spread * s / (scatterCount-1) : 0); var sb = new Bullet(); sb.x = hero.x; sb.y = hero.y; sb.pierce = Bullet.prototype.pierce || 1; sb.dirX = Math.cos(angle); sb.dirY = Math.sin(angle); sb.rotation = angle; bullets.push(sb); game.addChild(sb); } } }
User prompt
make 2 center bullets home paralelly when the bullet count is odd // Updated firing logic: when homing is active and even number of bullets, fire the two center bullets in parallel // inside your fire routine, replace the existing even-bullet block with this: var shotAngle = baseAngle; var dirX = Math.cos(shotAngle); var dirY = Math.sin(shotAngle); if (Bullet.prototype.homing && totalBullets % 2 === 0 && totalBullets > 1) { // 1) Fire the two center homing bullets in parallel fireBullet(dirX, dirY); fireBullet(dirX, dirY); // 2) Fire the remaining bullets in a symmetric scatter around the parallel pair var scatterCount = totalBullets - 2; if (scatterCount > 0) { // compute spread angle based on level (or fallback) var spread = Math.PI / 32 + Math.max(0, level - 2) * Math.PI / 48; spread = Math.min(spread, Math.PI / 4); for (var i = 0; i < scatterCount; i++) { var angle = shotAngle - spread / 2 + scatterCount > 1 ? spread * i / (scatterCount - 1) : 0; angle += shotAngle; fireBullet(Math.cos(angle), Math.sin(angle)); } } } else if (totalBullets % 2 === 0 && totalBullets > 1) { // ... your original even-bullet + scatter logic here ... } else { // ... other firing cases (odd counts, no homing, etc.) ... }
Code edit (1 edits merged)
Please save this source code
User prompt
shoot 3 extra bullet from the start
User prompt
set 4 bullets from the start
User prompt
set missile active from the start
User prompt
make it %10
User prompt
make it %5
User prompt
set powerup drop rate to %50
User prompt
add new collectable powerup which grants %100 movement speed for 5 seconds with its unique asset
Code edit (1 edits merged)
Please save this source code
User prompt
add new upgrade option which gives permanent movement speed buff to hero
User prompt
play game over sound when hero dies
Code edit (1 edits merged)
Please save this source code
User prompt
add new upgrade option which gives permanent +2 ammo size
User prompt
make ammo size 10 instead of 20
User prompt
make powerup indicators as before just text
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // 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._spawnTick = typeof ticksSurvived === "number" ? ticksSurvived : 0; self._flashStarted = false; self._flashTick = 0; self._flashVisible = true; self.update = function () { // Defensive: only run if ticksSurvived is available if (typeof ticksSurvived !== "number") { return; } var lifetime = 1200; // 20 seconds * 60 FPS var flashStart = 300; // 5 seconds * 60 FPS (start flashing at 15s, i.e. last 5s) var ticksAlive = ticksSurvived - self._spawnTick; // Start flashing in last 5 seconds (after 15s) if (ticksAlive >= lifetime - flashStart) { if (!self._flashStarted) { self._flashStarted = true; self._flashTick = 0; } // Pause flashing if popup is open var popupOpen = typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent; if (!popupOpen) { // Accelerate flash frequency as time runs out var ticksLeft = lifetime - ticksAlive; // Frequency: from 10 frames (0.16s) at 5s left, down to 2 frames (0.033s) at 0s left var minFlash = 2, maxFlash = 10; var flashInterval = Math.max(minFlash, Math.floor(maxFlash * (ticksLeft / flashStart))); self._flashTick++; if (self._flashTick >= flashInterval) { self._flashVisible = !self._flashVisible; self._flashTick = 0; } } self.visible = self._flashVisible; } else { // Not flashing yet self.visible = true; } // Destroy after 20 seconds if (ticksAlive >= lifetime) { if (self.parent) { self.destroy(); } // Remove from powerups array in main game loop (handled there) } }; 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 }); // Play fire sound every time a Bullet is created var fireSound = LK.getSound && LK.getSound('fire'); if (fireSound && typeof fireSound.play === "function") { fireSound.play(); } // Apply bullet size multiplier if present var sizeMultiplier = typeof Bullet.prototype.sizeMultiplier === "number" ? Bullet.prototype.sizeMultiplier : 1; if (sizeMultiplier !== 1) { bulletSprite.width *= sizeMultiplier; bulletSprite.height *= sizeMultiplier; } // 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; }); // FemaleZombie class: maintains distance from hero and fires projectiles var FemaleZombie = Container.expand(function () { var self = Container.call(this); // Attach unique female zombie asset var femaleZombieSpriteRight = self.attachAsset('female_zombie_right', { anchorX: 0.5, anchorY: 0.5 }); femaleZombieSpriteRight.assetId = 'female_zombie_right'; var femaleZombieSpriteLeft = self.attachAsset('female_zombie_left', { anchorX: 0.5, anchorY: 0.5 }); femaleZombieSpriteLeft.assetId = 'female_zombie_left'; femaleZombieSpriteLeft.visible = false; self.radius = Math.max(femaleZombieSpriteRight.width, femaleZombieSpriteRight.height) / 2; self.speed = 1.2; self.hp = 1; self._lastFacingRight = true; self._fireCooldown = 0; self._fireInterval = 600; // fires every 10 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) { femaleZombieSpriteRight.visible = shouldFaceRight; femaleZombieSpriteLeft.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 / 4; // 45 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, { femaleZombie: true }); 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; }); // 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._spawnTick = typeof ticksSurvived === "number" ? ticksSurvived : 0; self._flashStarted = false; self._flashTick = 0; self._flashVisible = true; self.update = function () { // Defensive: only run if ticksSurvived is available if (typeof ticksSurvived !== "number") { return; } var lifetime = 1200; // 20 seconds * 60 FPS var flashStart = 300; // 5 seconds * 60 FPS (start flashing at 15s, i.e. last 5s) var ticksAlive = ticksSurvived - self._spawnTick; // Start flashing in last 5 seconds (after 15s) if (ticksAlive >= lifetime - flashStart) { if (!self._flashStarted) { self._flashStarted = true; self._flashTick = 0; } // Pause flashing if popup is open var popupOpen = typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent; if (!popupOpen) { // Accelerate flash frequency as time runs out var ticksLeft = lifetime - ticksAlive; // Frequency: from 10 frames (0.16s) at 5s left, down to 2 frames (0.033s) at 0s left var minFlash = 2, maxFlash = 10; var flashInterval = Math.max(minFlash, Math.floor(maxFlash * (ticksLeft / flashStart))); self._flashTick++; if (self._flashTick >= flashInterval) { self._flashVisible = !self._flashVisible; self._flashTick = 0; } } self.visible = self._flashVisible; } else { // Not flashing yet self.visible = true; } // Destroy after 20 seconds if (ticksAlive >= lifetime) { if (self.parent) { self.destroy(); } // Remove from powerups array in main game loop (handled there) } }; 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; } } // --- Collision resolution (pushback) with all enemies --- if (typeof enemies !== "undefined" && Array.isArray(enemies)) { for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (!e || typeof e.x !== "number" || typeof e.y !== "number" || typeof e.radius !== "number" || typeof self.radius !== "number") { continue; } // Only resolve if overlapping var dx = self.x - e.x; var dy = self.y - e.y; var dist = Math.sqrt(dx * dx + dy * dy); var minDist = self.radius + e.radius; if (dist > 0 && dist < minDist) { // Push hero out of enemy, but only move hero (not enemy) var overlap = minDist - dist + 0.1; // add small epsilon to prevent sticking var pushX = dx / dist * overlap; var pushY = dy / dist * overlap; self.x += pushX; self.y += pushY; } } } if (typeof bigEnemies !== "undefined" && Array.isArray(bigEnemies)) { for (var i = 0; i < bigEnemies.length; i++) { var e = bigEnemies[i]; if (!e || typeof e.x !== "number" || typeof e.y !== "number" || typeof e.radius !== "number" || typeof self.radius !== "number") { continue; } // Only resolve if overlapping var dx = self.x - e.x; var dy = self.y - e.y; var dist = Math.sqrt(dx * dx + dy * dy); var minDist = self.radius + e.radius; if (dist > 0 && dist < minDist) { // Push hero out of big enemy, but only move hero (not enemy) var overlap = minDist - dist + 0.1; var pushX = dx / dist * overlap; var pushY = dy / dist * overlap; self.x += pushX; self.y += pushY; } } } // --- 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 --- // Pause flashing if popup is open var popupOpen = typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent; // Make the red glow flash repeatedly (pulsing) when at 1 life if (typeof heroLives !== "undefined" && heroLives === 1) { if (!self._redGlowActive) { self._redGlowActive = true; self._redGlowFlashTick = 0; self._redGlowFlashState = false; self._redGlowFlashInterval = 30; // 0.5s at 60fps LK.effects.flashObject(self, 0xff0000, 400); // initial flash, longer duration } // Only flash if no popup is open if (!popupOpen) { // Flash every interval self._redGlowFlashTick = (self._redGlowFlashTick || 0) + 1; if (self._redGlowFlashTick >= self._redGlowFlashInterval) { self._redGlowFlashTick = 0; self._redGlowFlashState = !self._redGlowFlashState; if (self._redGlowFlashState) { LK.effects.flashObject(self, 0xff0000, 400); // longer red duration } else { LK.effects.flashObject(self, 0xffffff, 100); // short white duration } } } } 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; self._redGlowFlashTick = 0; self._redGlowFlashState = 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; }); // MaleZombie class (formerly Enemy): normal enemy, zombie_dog, and big_zombie (boss) logic var MaleZombie = 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('phase_boss_right', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteRight.assetId = 'phase_boss_right'; var bossSpriteLeft = self.attachAsset('phase_boss_left', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteLeft.assetId = 'phase_boss_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.5; self.hp = 20 * 5; // Phase Boss HP (should be set by spawner too) self._bossSpriteRight = bossSpriteRight; self._bossSpriteLeft = bossSpriteLeft; } else { var bossSpriteRight = self.attachAsset('big_zombie', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteRight.assetId = 'big_zombie'; var bossSpriteLeft = self.attachAsset('big_zombie_left', { anchorX: 0.5, anchorY: 0.5 }); bossSpriteLeft.assetId = 'big_zombie_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.5; self.hp = 20; self._bossSpriteRight = bossSpriteRight; self._bossSpriteLeft = bossSpriteLeft; } } else { // If opts.fastEnemy is set, use zombie_dog assets if (opts && opts.fastEnemy) { var enemySpriteRight = self.attachAsset('zombie_dog_right', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteRight.assetId = 'zombie_dog_right'; var enemySpriteLeft = self.attachAsset('zombie_dog_left', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteLeft.assetId = 'zombie_dog_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.hp = 1; self._enemySpriteRight = enemySpriteRight; self._enemySpriteLeft = enemySpriteLeft; self.isFastEnemy = true; } else { var enemySpriteRight = self.attachAsset('male_zombie_right', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteRight.assetId = 'male_zombie_right'; var enemySpriteLeft = self.attachAsset('male_zombie_left', { anchorX: 0.5, anchorY: 0.5 }); enemySpriteLeft.assetId = 'male_zombie_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; 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; } // --- Phase Boss projectile attack --- if (self.isFinalBoss) { // Track projectile cooldown if (typeof self._projectileCooldown === "undefined") { self._projectileCooldown = 0; } self._projectileCooldown--; if (self._projectileCooldown <= 0) { self._projectileCooldown = 300; // Fire every 5 seconds (300 ticks) // Fire 3 projectiles in a spread toward hero if (typeof game !== "undefined" && typeof RangedEnemyProjectile === "function") { var angleToHero = Math.atan2(hero.y - self.y, hero.x - self.x); var spread = Math.PI / 4; // 45 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, { phaseBoss: true }); // Make projectile 2x larger than female zombie projectile, then lower by 30% (final: 1.4x original) if (proj && proj.children && proj.children.length > 0) { for (var c = 0; c < proj.children.length; c++) { proj.children[c].width *= 1.4; proj.children[c].height *= 1.4; } // Update radius to match new size (hitbox matches asset size) proj.radius = Math.max(proj.children[0].width, proj.children[0].height) / 2; } game.addChild(proj); if (typeof enemyProjectiles !== "undefined") { enemyProjectiles.push(proj); } } } } } } else { if (shouldFaceRight !== self._lastFacingRight) { self._enemySpriteRight.visible = shouldFaceRight; self._enemySpriteLeft.visible = !shouldFaceRight; self._lastFacingRight = shouldFaceRight; } } }; return self; }); // MoveSpeedPowerup class (grants 100% movement speed for 5 seconds) var MoveSpeedPowerup = Container.expand(function () { var self = Container.call(this); // Use unique move_speed_powerup asset var sprite = self.attachAsset('move_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._spawnTick = typeof ticksSurvived === "number" ? ticksSurvived : 0; self._flashStarted = false; self._flashTick = 0; self._flashVisible = true; self.update = function () { // Defensive: only run if ticksSurvived is available if (typeof ticksSurvived !== "number") { return; } var lifetime = 1200; // 20 seconds * 60 FPS var flashStart = 300; // 5 seconds * 60 FPS (start flashing at 15s, i.e. last 5s) var ticksAlive = ticksSurvived - self._spawnTick; // Start flashing in last 5 seconds (after 15s) if (ticksAlive >= lifetime - flashStart) { if (!self._flashStarted) { self._flashStarted = true; self._flashTick = 0; } // Pause flashing if popup is open var popupOpen = typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent; if (!popupOpen) { // Accelerate flash frequency as time runs out var ticksLeft = lifetime - ticksAlive; // Frequency: from 10 frames (0.16s) at 5s left, down to 2 frames (0.033s) at 0s left var minFlash = 2, maxFlash = 10; var flashInterval = Math.max(minFlash, Math.floor(maxFlash * (ticksLeft / flashStart))); self._flashTick++; if (self._flashTick >= flashInterval) { self._flashVisible = !self._flashVisible; self._flashTick = 0; } } self.visible = self._flashVisible; } else { // Not flashing yet self.visible = true; } // Destroy after 20 seconds if (ticksAlive >= lifetime) { if (self.parent) { self.destroy(); } // Remove from powerups array in main game loop (handled there) } }; 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._spawnTick = typeof ticksSurvived === "number" ? ticksSurvived : 0; self._flashStarted = false; self._flashTick = 0; self._flashVisible = true; self.update = function () { // Defensive: only run if ticksSurvived is available if (typeof ticksSurvived !== "number") { return; } var lifetime = 1200; // 20 seconds * 60 FPS var flashStart = 300; // 5 seconds * 60 FPS (start flashing at 15s, i.e. last 5s) var ticksAlive = ticksSurvived - self._spawnTick; // Start flashing in last 5 seconds (after 15s) if (ticksAlive >= lifetime - flashStart) { if (!self._flashStarted) { self._flashStarted = true; self._flashTick = 0; } // Pause flashing if popup is open var popupOpen = typeof startOptionPopup !== "undefined" && startOptionPopup && startOptionPopup.parent || typeof levelUpPopup !== "undefined" && levelUpPopup && levelUpPopup.parent; if (!popupOpen) { // Accelerate flash frequency as time runs out var ticksLeft = lifetime - ticksAlive; // Frequency: from 10 frames (0.16s) at 5s left, down to 2 frames (0.033s) at 0s left var minFlash = 2, maxFlash = 10; var flashInterval = Math.max(minFlash, Math.floor(maxFlash * (ticksLeft / flashStart))); self._flashTick++; if (self._flashTick >= flashInterval) { self._flashVisible = !self._flashVisible; self._flashTick = 0; } } self.visible = self._flashVisible; } else { // Not flashing yet self.visible = true; } // Destroy after 20 seconds if (ticksAlive >= lifetime) { if (self.parent) { self.destroy(); } // Remove from powerups array in main game loop (handled there) } }; return self; }); // RangedEnemyProjectile class (simple straight projectile) var RangedEnemyProjectile = Container.expand(function (startX, startY, targetX, targetY, opts) { var self = Container.call(this); // Use correct asset for each projectile type var sprite; if (opts && opts.femaleZombie) { sprite = self.attachAsset('female_zombie_projectile', { anchorX: 0.5, anchorY: 0.5 }); } else if (opts && opts.phaseBoss) { sprite = self.attachAsset('phase_boss_projectile', { anchorX: 0.5, anchorY: 0.5 }); } else { sprite = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); sprite.tint = 0xff3333; } // Set radius to match asset size (hitbox matches asset size) 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 ****/ // unique asset for movement speed powerup // Enable child sorting by zIndex for popups and overlays // Unique ranged enemy assets game.sortChildrenEnabled = true; // fast enemy left // fast enemy right // Set Bullet.prototype.homingStrength to control how fast homing bullets turn (0.0 = no turn, 1.0 = instant turn) // Example: Bullet.prototype.homingStrength = 0.08; // default is 0.08 for smooth, natural turning Bullet.prototype.homingStrength = 0.008; // Lower value for even smoother, slower homing Bullet.prototype.homing = false; // Missile (homing) is NOT active from the start window._missileChosen = false; // Allow 'Missile' to appear as an upgrade option // 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; // All normal enemies (male zombie, female zombie, zombie_dog) are grouped in this array var enemies = []; // All big enemies (big_zombie and future big enemies) are grouped in this array var bigEnemies = []; var bullets = []; var enemyProjectiles = []; // For RangedEnemy projectiles var gems = []; var powerups = []; var spawnTimer = 0; var spawnInterval = 180; // doubled from 90 to reduce frequency by 50% 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); // --- Reloading text below hero --- var reloadingBelowHeroTxt = new Text2('Reloading', { size: 50, fill: 0xFFD700, font: "Montserrat" }); reloadingBelowHeroTxt.anchor.set(0.5, 0); reloadingBelowHeroTxt.visible = false; reloadingBelowHeroTxt.x = centerX; reloadingBelowHeroTxt.y = centerY + 120; game.addChild(reloadingBelowHeroTxt); // --- Shoot countdown text below hero --- var shootCountdownTxt = new Text2('', { size: 50, fill: 0xFFD700, font: "Montserrat" }); shootCountdownTxt.anchor.set(0.5, 0); shootCountdownTxt.visible = true; shootCountdownTxt.x = centerX; shootCountdownTxt.y = centerY + 120; game.addChild(shootCountdownTxt); // --- Option popup at game start --- var startOptionPopup; function showStartOptionPopup() { // Freeze all game logic and input game._levelUpFrozen = true; // Pause music when popup is shown LK.stopMusic(); // 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); } }, { label: 'Bullet Size Up', desc: 'Bullets are 40% bigger!', color: 0xA0C8FF, onSelect: function onSelect() { if (typeof Bullet.prototype.sizeMultiplier === "undefined") { Bullet.prototype.sizeMultiplier = 1.4; } else { Bullet.prototype.sizeMultiplier *= 1.4; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xA0C8FF, 3000); } }, { label: 'Ammo +2', desc: 'Increases ammo size by 2', color: 0xB0FFB0, onSelect: function onSelect() { if (typeof heroBulletMax === "number") { heroBulletMax += 2; heroBulletCount = heroBulletMax; reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax); } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xB0FFB0, 3000); } }, { label: 'Movement Speed', desc: 'Hero moves faster', color: 0x7B9EFF, onSelect: function onSelect() { if (typeof hero.speed === "number") { hero.speed += 1.5; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0x7B9EFF, 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; // Resume music after closing the start option popup LK.playMusic('8-BitZombie'); }; }(opt); startOptionPopup.addChild(optContainer); } // Add popup to game startOptionPopup.zIndex = 1000; game.addChild(startOptionPopup); // Always keep popup on top if (game.children && game.children.length > 1) { game.removeChild(startOptionPopup); game.addChild(startOptionPopup); } } // Show the popup at game start hero = new Hero(); hero.x = centerX; hero.y = centerY; game.addChild(hero); // Play 8-BitZombie music at game start LK.playMusic('8-BitZombie'); 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); // Heart UI: display hearts equal to heroLives var heartUI = new Container(); heartUI.y = 10 + 60 - 10; heartUI.x = -25; LK.gui.topRight.addChild(heartUI); // Helper to update heart UI function updateHeartUI() { // Remove all previous hearts while (heartUI.children.length > 0) { heartUI.removeChild(heartUI.children[0]); } // Add one heart asset per life var spacing = 60; for (var i = 0; i < heroLives; i++) { var heart = LK.getAsset('heart_powerup', { anchorX: 1, anchorY: 0 }); // Scale down if needed for UI var scale = 0.6; heart.width *= scale; heart.height *= scale; heart.x = -i * (heart.width + 8); heart.y = 0; heartUI.addChild(heart); } } updateHeartUI(); 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); // Container for XP indicator: big_xp_gem asset + text var xpIndicator = new Container(); xpIndicator.y = heartUI.y + heartUI.height + 8; xpIndicator.x = -25 - 25 + 5; // Add XP text first (to the right) xpTxt = new Text2('0/10', { size: 44, fill: 0x7BE495, font: "Montserrat" }); xpTxt.anchor.set(1, 0); xpTxt.x = 0; xpTxt.y = 0; xpIndicator.addChild(xpTxt); // Add colon text between gem and XP text var xpColonTxt = new Text2(':', { size: 44, fill: 0x7BE495, font: "Montserrat" }); xpColonTxt.anchor.set(1, 0); xpColonTxt.x = -xpTxt.width - 5; xpColonTxt.y = 0; xpIndicator.addChild(xpColonTxt); // Add big_xp_gem asset to the left of the colon var xpGemAsset = LK.getAsset('big_xp_gem', { anchorX: 1, anchorY: 0 }); var xpGemScale = 0.7; xpGemAsset.width *= xpGemScale; xpGemAsset.height *= xpGemScale; xpGemAsset.x = -xpTxt.width - xpColonTxt.width - 10; xpGemAsset.y = 0; xpIndicator.addChild(xpGemAsset); LK.gui.topRight.addChild(xpIndicator); 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; // Remove from GUI, will be positioned above hero in game.update magnetTimerTxt.y = 0; attackSpeedTimerTxt.y = 0; // Now that magnetTimerTxt and attackSpeedTimerTxt are not in GUI, set achievement50Txt.y below levelTxt achievement50Txt.y = levelTxt.y + levelTxt.height + 20; // Spawns a normal enemy (male zombie, female zombie, or zombie_dog) and adds to the enemies array 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; } // 6% chance to make this enemy a rare zombie_dog (speed 5, not a boss/final boss) var isZombieDog = false; if (Math.random() < 0.06) { isZombieDog = true; } // 6% chance to spawn a FemaleZombie, otherwise normal/zombie_dog enemy var enemy; if (Math.random() < 0.06) { enemy = new FemaleZombie(); } else { enemy = new MaleZombie({ fastEnemy: isZombieDog }); } // No need to set speed here, handled in MaleZombie 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 === 'zombie_dog_right') { child.visible = true; } if (child.assetId === 'enemy_left' || child.assetId === 'zombie_dog_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 === 'zombie_dog_right') { child.visible = false; } if (child.assetId === 'enemy_left' || child.assetId === 'zombie_dog_left') { child.visible = true; } } } enemy._lastFacingRight = false; } enemy.x = x; enemy.y = y; // All normal enemies (male zombie, female zombie, zombie_dog) are grouped in the enemies array 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); } // Spawn move speed powerup function spawnMoveSpeedPowerup(x, y) { var msp = new MoveSpeedPowerup(); msp.x = x; msp.y = y; powerups.push(msp); game.addChild(msp); } 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); // Play fire sound every time hero shoots var fireSound = LK.getSound && LK.getSound('fire'); if (fireSound && typeof fireSound.play === "function") { fireSound.play(); } 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 () { // --- Play lowhp sound continually when heroLives is 1 --- if (typeof heroLives !== "undefined" && heroLives === 1) { if (typeof window._lowhpSound === "undefined" || !window._lowhpSound) { var lowhpSound = LK.getSound && LK.getSound('lowhp'); if (lowhpSound && typeof lowhpSound.play === "function") { lowhpSound.loop = true; lowhpSound.play(); window._lowhpSound = lowhpSound; } } else { // Defensive: ensure loop is always true and sound is playing if (typeof window._lowhpSound.loop === "boolean" && !window._lowhpSound.loop) { window._lowhpSound.loop = true; } if (typeof window._lowhpSound.play === "function" && typeof window._lowhpSound.isPlaying === "function") { if (!window._lowhpSound.isPlaying()) { window._lowhpSound.play(); } } else if (typeof window._lowhpSound.play === "function" && !window._lowhpSound._playing) { window._lowhpSound.play(); } } } else { if (typeof window._lowhpSound !== "undefined" && window._lowhpSound) { if (typeof window._lowhpSound.stop === "function") { window._lowhpSound.stop(); } window._lowhpSound = undefined; } } // --- Phase Boss logic at 5 minutes --- if (typeof phaseBossActive === "undefined") { phaseBossActive = false; } if (typeof phaseBossDefeated === "undefined") { phaseBossDefeated = false; } if (typeof phaseBoss === "undefined") { phaseBoss = null; } if (typeof phaseBossTriggered === "undefined") { phaseBossTriggered = false; } // Trigger phase boss at 2 minutes (ticksSurvived >= 7200), only once if (!phaseBossTriggered && typeof ticksSurvived === "number" && ticksSurvived >= 7200 && !phaseBossActive && !phaseBossDefeated) { phaseBossTriggered = true; phaseBossActive = true; // Pause enemy wave spawning when phase boss appears, but do not kill/remove other enemies // (No code needed here, just do not remove any enemies) // Spawn phase boss at a random edge var boss = new MaleZombie({ isBoss: true, isFinalBoss: true }); 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; } bigEnemies.push(boss); game.addChild(boss); phaseBoss = boss; } 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(); // Update all normal enemies (male zombie, female zombie, zombie_dog) in the enemies array 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) { // Allow phase boss (final boss) to damage hero as well as all other enemies // 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); // Play damage taken sound var damageTakeSound = LK.getSound && LK.getSound('damage_take'); if (damageTakeSound && typeof damageTakeSound.play === "function") { damageTakeSound.play(); } heroLives--; updateHeartUI(); if (heroLives <= 0) { LK.showGameOver(); lastGameOver = true; } // No invulnerability after taking damage } } e.lastWasTouchingHero = isTouchingHero; } // Phase boss and big enemies collision with hero for (var i = bigEnemies.length - 1; i >= 0; i--) { var be = bigEnemies[i]; if (!be) { continue; } if (typeof be.lastWasTouchingHero === "undefined") { be.lastWasTouchingHero = false; } var isTouchingHero = dist2(be, hero) < be.radius + hero.radius; if (!be.lastWasTouchingHero && isTouchingHero) { // Check for hero invulnerability if (!(typeof hero._invulnerableUntilTick === "number" && typeof ticksSurvived === "number" && ticksSurvived < hero._invulnerableUntilTick)) { LK.effects.flashScreen(0xff0000, 1000); // Play damage taken sound var damageTakeSound = LK.getSound && LK.getSound('damage_take'); if (damageTakeSound && typeof damageTakeSound.play === "function") { damageTakeSound.play(); } heroLives--; updateHeartUI(); if (heroLives <= 0) { LK.showGameOver(); lastGameOver = true; } // No invulnerability after taking damage } } be.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); // Play damage taken sound var damageTakeSound = LK.getSound && LK.getSound('damage_take'); if (damageTakeSound && typeof damageTakeSound.play === "function") { damageTakeSound.play(); } heroLives--; updateHeartUI(); if (heroLives <= 0) { LK.showGameOver(); lastGameOver = true; } // No invulnerability after taking damage } 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; } // --- Ricochet & Pierce unified logic for all enemies (male, female, zombie dog, big, boss) --- var targets = enemies.concat(bigEnemies); for (var j = 0; j < targets.length; j++) { var e = targets[j]; // Defensive: skip if destroyed or not in game if (!e || typeof e.x !== "number" || typeof e.y !== "number" || typeof e.hp !== "number" || e.hp <= 0) { continue; } // Skip if already hit by this bullet if (b._hitEnemies && b._hitEnemies.indexOf(e) !== -1) { continue; } if (dist2(b, e) < b.radius + e.radius) { // 1) Mark as hit if (b._hitEnemies) { b._hitEnemies.push(e); } // 2) Damage e.hp--; // Play hit sound when enemy or boss takes damage or dies var hitSound = LK.getSound && LK.getSound('hit'); if (hitSound && typeof hitSound.play === "function") { hitSound.play(); } // Glow red when taking damage (including big enemies and bosses) LK.effects.flashObject(e, 0xff0000, 180); if (e.hp <= 0) { // --- Powerup drop logic for boss/normal --- var droppedPowerup = false; // Set powerup drop rate to 10% for all enemies and bosses var bossPowerupRate = 0.10; if (Math.random() < bossPowerupRate) { // Weighted random selection for all: [magnet:1, heart:1, attackSpeed:1, moveSpeed:1] var powerupWeights = [{ type: "magnet", weight: 1 }, { type: "heart", weight: 1 }, { type: "attackSpeed", weight: 1 }, { type: "moveSpeed", weight: 1 }]; var totalWeight = 0; for (var w = 0; w < powerupWeights.length; w++) { totalWeight += powerupWeights[w].weight; } var rand = Math.random() * totalWeight; var acc = 0, selectedType = null; for (var w = 0; w < powerupWeights.length; w++) { acc += powerupWeights[w].weight; if (rand < acc) { selectedType = powerupWeights[w].type; break; } } if (selectedType === "magnet") { spawnMagnetPowerup(e.x, e.y); } else if (selectedType === "heart") { spawnHeartPowerup(e.x, e.y); } else if (selectedType === "moveSpeed") { spawnMoveSpeedPowerup(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) { if (e.isBoss === true || e.isFinalBoss === true) { // Boss or Final Boss: drop 1 big xp gem and 5 normal gems 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); } } else { // All other enemies: 10% chance to drop a big XP gem, otherwise normal XP gem if (Math.random() < 0.10) { spawnGem(e.x, e.y, { type: 'big' }); } else { spawnGem(e.x, e.y); } } } enemyKillCount++; e.destroy(); // Remove Phase Boss reference if this was the Phase Boss if (e.isFinalBoss) { phaseBossActive = false; phaseBossDefeated = true; phaseBoss = null; // Resume enemy wave spawning after phase boss is killed // (No additional code needed, wave spawning resumes automatically when phaseBossActive is false) } // Remove from correct array var arr = e.isBoss === true || e.isFinalBoss === true ? bigEnemies : enemies; var idx = arr.indexOf(e); if (idx !== -1) { arr.splice(idx, 1); } } // 3) Ricochet? if (typeof b.ricochetLeft !== "undefined" && b.ricochetLeft > 0) { // Find nearest enemy (not already hit) var minDist = 99999, nearest = null; for (var k = 0; k < targets.length; k++) { var ricE = targets[k]; if (!ricE || typeof ricE.x !== "number" || typeof ricE.y !== "number" || typeof ricE.hp !== "number" || ricE.hp <= 0) { continue; } if (b._hitEnemies && b._hitEnemies.indexOf(ricE) !== -1) { continue; } var d = dist2(b, ricE); if (d < minDist) { minDist = d; nearest = ricE; } } if (nearest) { var dx = nearest.x - b.x, dy = nearest.y - b.y; var d = Math.hypot(dx, dy); if (d > 0) { b.dirX = dx / d; b.dirY = dy / d; b.rotation = Math.atan2(b.dirY, b.dirX); b.ricochetLeft--; b.pierce = Math.max(1, Bullet.prototype.pierce || 1); // Move slightly to avoid instant re-collision b.x += b.dirX * 10; b.y += b.dirY * 10; // Continue searching for collisions after ricochet continue; } } } // 4) Pierce logic: decrement, destroy if 0 b.pierce--; if (b.pierce <= 0) { b.destroy(); bullets.splice(i, 1); } break; // Only one enemy hit per bullet per frame } } } 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; // Play big_gem sound var bigGemSound = LK.getSound && LK.getSound('big_gem'); if (bigGemSound && typeof bigGemSound.play === "function") { bigGemSound.play(); } } else { xp += 1; // Play gem sound var gemSound = LK.getSound && LK.getSound('gem'); if (gemSound && typeof gemSound.play === "function") { gemSound.play(); } } 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, reduced by 20% xpToLevel = Math.floor((8 + level * 5 + Math.floor(level * level * 0.7)) * 0.8); 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; } // Pause music when popup is shown LK.stopMusic(); // 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); } }, { label: 'Bullet Size Up', desc: 'Bullets are 40% bigger!', color: 0xA0C8FF, onSelect: function onSelect() { if (typeof Bullet.prototype.sizeMultiplier === "undefined") { Bullet.prototype.sizeMultiplier = 1.4; } else { Bullet.prototype.sizeMultiplier *= 1.4; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xA0C8FF, 3000); } }, { label: 'Ammo +2', desc: 'Increases ammo size by 2', color: 0xB0FFB0, onSelect: function onSelect() { if (typeof heroBulletMax === "number") { heroBulletMax += 2; heroBulletCount = heroBulletMax; reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax); } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0xB0FFB0, 3000); } }, { label: 'Movement Speed', desc: 'Hero moves faster', color: 0x7B9EFF, onSelect: function onSelect() { if (typeof hero.speed === "number") { hero.speed += 1.5; } hero._invulnerableUntilTick = (typeof ticksSurvived === "number" ? ticksSurvived : 0) + 180; LK.effects.flashObject(hero, 0x7B9EFF, 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; // Resume music after closing the level up popup LK.playMusic('8-BitZombie'); }; }(opt); levelUpPopup.addChild(optContainer); } // Add popup to game levelUpPopup.zIndex = 1000; game.addChild(levelUpPopup); // Always keep popup on top if (game.children && game.children.length > 1) { game.removeChild(levelUpPopup); 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); // Play magnet sound var magnetSound = LK.getSound && LK.getSound('magnet'); if (magnetSound && typeof magnetSound.play === "function") { magnetSound.play(); } 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; } updateHeartUI(); // Play life sound var lifeSound = LK.getSound && LK.getSound('life'); if (lifeSound && typeof lifeSound.play === "function") { lifeSound.play(); } 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); // Play attack speed sound var attackSpeedSound = LK.getSound && LK.getSound('attack_speed'); if (attackSpeedSound && typeof attackSpeedSound.play === "function") { attackSpeedSound.play(); } } // Move speed powerup pickup else if (p.constructor === MoveSpeedPowerup) { // Set move speed boost for 5 seconds (300 ticks), do not stack duration, just reset to 5s hero._moveSpeedBoostEndTick = ticksSurvived + 300; LK.effects.flashObject(hero, 0x7B9EFF, 800); // Play attack speed sound as placeholder (replace with move speed sound if available) var attackSpeedSound = LK.getSound && LK.getSound('attack_speed'); if (attackSpeedSound && typeof attackSpeedSound.play === "function") { attackSpeedSound.play(); } } // 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; } } // Handle move speed boost effect if (typeof hero._moveSpeedBoostEndTick !== "undefined" && typeof ticksSurvived !== "undefined" && ticksSurvived < hero._moveSpeedBoostEndTick) { if (typeof hero._moveSpeedBoostActive === "undefined" || !hero._moveSpeedBoostActive) { hero._moveSpeedBoostActive = true; hero._originalSpeed = hero.speed; hero.speed = (typeof hero._originalSpeed === "number" ? hero._originalSpeed : hero.speed) * 2; } } else if (typeof hero._moveSpeedBoostActive !== "undefined" && hero._moveSpeedBoostActive) { hero._moveSpeedBoostActive = false; if (typeof hero._originalSpeed !== "undefined") { hero.speed = hero._originalSpeed; } } 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; // Search all enemies, including bigEnemies (bosses) for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; // Only consider enemies that are onscreen if (!e || typeof e.x !== "number" || typeof e.y !== "number" || typeof e.hp !== "number" || e.hp <= 0 || e.x < 0 || e.x > 2048 || e.y < 0 || e.y > 2732) { continue; } var d = dist2(hero, e); if (d < minDist) { minDist = d; nearest = e; } } for (var i = 0; i < bigEnemies.length; i++) { var e = bigEnemies[i]; // Defensive: skip if destroyed or not in game if (!e || typeof e.x !== "number" || typeof e.y !== "number" || typeof e.hp !== "number" || e.hp <= 0 || e.x < 0 || e.x > 2048 || e.y < 0 || e.y > 2732) { continue; } 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 sound removed) // 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); } } // Decrement heroBulletCount only after the entire volley is fired if (volleyIdx + 1 === shotsToFire || heroBulletCount - 1 <= 0) { heroBulletCount--; reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax); // If out of ammo after this volley, 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 && !(Bullet.prototype.homing && window._missileChosen && totalBullets === 2)) { 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++) { // Play fire sound for every single bullet shot var fireSound = LK.getSound && LK.getSound('fire'); if (fireSound && typeof fireSound.play === "function") { fireSound.play(); } 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 (Bullet.prototype.homing && window._missileChosen && totalBullets % 2 === 1 && totalBullets > 1) { // Missile is active, bullet count is odd: do NOT separate two center bullets, fire all in a spread from center 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++) { // Play fire sound for every single bullet shot var fireSound = LK.getSound && LK.getSound('fire'); if (fireSound && typeof fireSound.play === "function") { fireSound.play(); } 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 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++) { // Play fire sound for every single bullet shot var fireSound = LK.getSound && LK.getSound('fire'); if (fireSound && typeof fireSound.play === "function") { fireSound.play(); } 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++) { // Play fire sound for every single bullet shot var fireSound = LK.getSound && LK.getSound('fire'); if (fireSound && typeof fireSound.play === "function") { fireSound.play(); } 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); } } // Decrement heroBulletCount only after the entire volley is fired 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 Phase Boss is active if (!phaseBossActive) { spawnTimer++; if (spawnTimer >= spawnInterval) { spawnTimer = 0; var toSpawn = Math.max(1, Math.floor((1 + Math.floor(wave / 3)) / 4 * 0.75 / 2)); // halve the number of spawns for (var i = 0; i < toSpawn; i++) { spawnEnemy(); } // Boss spawn logic: spawn boss every 30 waves if (typeof bossSpawnedWaves === "undefined") { bossSpawnedWaves = {}; } // Removed big zombie spawn on wave 5 // Boss spawn logic: spawn boss every 30 waves if (wave % 30 === 0 && !bossSpawnedWaves[wave]) { var boss = new MaleZombie({ 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; } bigEnemies.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 + '/' + 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; } updateHeartUI(); // Reset bullet count and reload state heroBulletCount = heroBulletMax; heroReloading = false; reloadTxt.setText(''); if (heroReloadTimeout) { LK.clearTimeout(heroReloadTimeout); heroReloadTimeout = null; } } // Magnet/Attack Speed timer display above hero (text only, no icons) if (hero && hero.parent) { // Place both timer texts further above hero's head, stacking vertically var aboveY = hero.y - (typeof hero.radius === "number" ? hero.radius : 70) - 48; var timerSpacing = 12; // --- Magnet Powerup Timer (text only) --- if (hero.magnetActive && hero.magnetDuration > 0) { var magnetSeconds = Math.ceil(hero.magnetDuration / 60); magnetTimerTxt.setText('Magnet: ' + magnetSeconds + 's'); magnetTimerTxt.visible = true; magnetTimerTxt.anchor.set(0.5, 0.5); magnetTimerTxt.x = hero.x; magnetTimerTxt.y = aboveY; if (!magnetTimerTxt.parent) { game.addChild(magnetTimerTxt); } aboveY -= magnetTimerTxt.height + timerSpacing; } else { magnetTimerTxt.setText(''); magnetTimerTxt.visible = false; if (magnetTimerTxt.parent) { magnetTimerTxt.parent.removeChild(magnetTimerTxt); } } // --- Attack Speed Powerup Timer (text only) --- 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'); attackSpeedTimerTxt.visible = true; attackSpeedTimerTxt.anchor.set(0.5, 0.5); attackSpeedTimerTxt.x = hero.x; attackSpeedTimerTxt.y = aboveY; if (!attackSpeedTimerTxt.parent) { game.addChild(attackSpeedTimerTxt); } aboveY -= attackSpeedTimerTxt.height + timerSpacing; } else { attackSpeedTimerTxt.setText(''); attackSpeedTimerTxt.visible = false; if (attackSpeedTimerTxt.parent) { attackSpeedTimerTxt.parent.removeChild(attackSpeedTimerTxt); } } // --- Move Speed Powerup Timer (text only) --- if (typeof hero._moveSpeedBoostEndTick !== "undefined" && typeof ticksSurvived !== "undefined" && ticksSurvived < hero._moveSpeedBoostEndTick) { if (typeof moveSpeedTimerTxt === "undefined") { moveSpeedTimerTxt = new Text2('', { size: 44, fill: 0x7B9EFF, font: "Montserrat" }); moveSpeedTimerTxt.anchor.set(0.5, 0.5); } var msSeconds = Math.ceil((hero._moveSpeedBoostEndTick - ticksSurvived) / 60); moveSpeedTimerTxt.setText('Move Speed: ' + msSeconds + 's'); moveSpeedTimerTxt.visible = true; moveSpeedTimerTxt.x = hero.x; moveSpeedTimerTxt.y = aboveY; if (!moveSpeedTimerTxt.parent) { game.addChild(moveSpeedTimerTxt); } aboveY -= moveSpeedTimerTxt.height + timerSpacing; } else { if (typeof moveSpeedTimerTxt !== "undefined") { moveSpeedTimerTxt.setText(''); moveSpeedTimerTxt.visible = false; if (moveSpeedTimerTxt.parent) { moveSpeedTimerTxt.parent.removeChild(moveSpeedTimerTxt); } } } } else { magnetTimerTxt.visible = false; attackSpeedTimerTxt.visible = false; if (magnetTimerTxt.parent) { magnetTimerTxt.parent.removeChild(magnetTimerTxt); } if (attackSpeedTimerTxt.parent) { attackSpeedTimerTxt.parent.removeChild(attackSpeedTimerTxt); } if (typeof magnetTimerContainer !== "undefined" && magnetTimerContainer.parent) { magnetTimerContainer.parent.removeChild(magnetTimerContainer); } if (typeof attackSpeedTimerContainer !== "undefined" && attackSpeedTimerContainer.parent) { attackSpeedTimerContainer.parent.removeChild(attackSpeedTimerContainer); } } // Show ammo count if not reloading if (!heroReloading) { reloadTxt.setText('Ammo: ' + heroBulletCount + '/' + heroBulletMax); } // --- Show/hide and update 'Reloading' text below hero --- if (heroReloading && hero && hero.parent) { reloadingBelowHeroTxt.visible = true; reloadingBelowHeroTxt.x = hero.x; // Place below hero's feet, offset by hero's radius + 18px (same as shootCountdownTxt) reloadingBelowHeroTxt.y = hero.y + (typeof hero.radius === "number" ? hero.radius : 70) + 18; } else { reloadingBelowHeroTxt.visible = false; } // --- Update shoot countdown text below hero --- if (hero && hero.parent) { if (!heroReloading) { shootCountdownTxt.visible = true; shootCountdownTxt.x = hero.x; // Place below hero's feet, offset by hero's radius + 18px shootCountdownTxt.y = hero.y + (typeof hero.radius === "number" ? hero.radius : 70) + 18; shootCountdownTxt.setText('Ammo: ' + heroBulletCount); } else { shootCountdownTxt.visible = false; } } };
===================================================================
--- original.js
+++ change.js
@@ -1048,19 +1048,19 @@
/****
* Game Code
****/
-// Unique ranged enemy assets
-// Enable child sorting by zIndex for popups and overlays
// unique asset for movement speed powerup
+// Enable child sorting by zIndex for popups and overlays
+// Unique ranged enemy assets
game.sortChildrenEnabled = true;
// fast enemy left
// fast enemy right
// Set Bullet.prototype.homingStrength to control how fast homing bullets turn (0.0 = no turn, 1.0 = instant turn)
// Example: Bullet.prototype.homingStrength = 0.08; // default is 0.08 for smooth, natural turning
Bullet.prototype.homingStrength = 0.008; // Lower value for even smoother, slower homing
-Bullet.prototype.homing = true; // Missile (homing) is now active from the start
-window._missileChosen = true; // Prevents 'Missile' from appearing as an upgrade option
+Bullet.prototype.homing = false; // Missile (homing) is NOT active from the start
+window._missileChosen = false; // Allow 'Missile' to appear as an upgrade option
// Add background image to the game scene
var background = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
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