/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (initialSpeed) {
var self = Container.call(this);
// Added initialSpeed parameter
//Create and attach asset.
self.visual = self.attachAsset('bullet', {
// Uses the existing 'bullet' image asset
anchorX: 0.5,
anchorY: 0.5
});
self.speed = initialSpeed; // Use the passed initialSpeed for movement
// If this instance of bullet is attached, this method will be called every tick by the LK engine automatically.
self.update = function () {
self.y += self.speed;
};
return self;
});
// Coin collectible class (was PointCollectible)
var CoinCollectible = Container.expand(function () {
var self = Container.call(this);
var coin = self.attachAsset('coin_icon', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player character controlled by face
var FaceChar = Container.expand(function () {
var self = Container.call(this);
var _char = self.attachAsset('faceChar', {
anchorX: 0.5,
anchorY: 0.5
});
// For possible future effects
self.flash = function () {
LK.effects.flashObject(self, 0xffffff, 300);
};
return self;
});
var Gun = Container.expand(function () {
var self = Container.call(this);
// The visual asset for the gun
self.visual = self.attachAsset('gun_visual', {
anchorX: 0.5,
// Anchor to its horizontal center
anchorY: 1.0 // Anchor to its bottom edge (base of the gun)
});
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obs = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
// Speed is set on creation
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Shop class for purchasing items
var Shop = Container.expand(function () {
var self = Container.call(this);
var items = [{
name: "Speed Boost",
cost: 10
}, {
name: "Extra Life",
cost: 20
}, {
name: "Shield",
cost: 15
}];
var shopText = new Text2("Shop", {
size: 100,
fill: 0xFFFFFF
});
shopText.anchor.set(0.5, 0);
self.addChild(shopText);
shopText.y = -200;
// Show current persistent points
var pointsText = new Text2("", {
size: 70,
fill: 0xFFFF00
});
pointsText.anchor.set(0.5, 0);
pointsText.y = -100;
self.addChild(pointsText);
function updatePointsText() {
var totalPoints = 0;
var extraLives = 0;
var shields = 0;
if (typeof storage !== "undefined") {
totalPoints = storage.totalPoints || 0;
extraLives = typeof storage.extraLives !== "undefined" ? storage.extraLives : 3;
shields = storage.shields || 0;
}
pointsText.setText("Points: " + totalPoints + " | Extra Lives: " + extraLives + " | Shields: " + shields);
// Also update global scoreTxt if shop is open
if (typeof scoreTxt !== "undefined") {
scoreTxt.setText(totalPoints);
}
}
updatePointsText();
// Add shop items and purchase logic
items.forEach(function (item, index) {
var itemText = new Text2(item.name + " - " + item.cost + " points", {
size: 70,
fill: 0xFFFFFF
});
itemText.anchor.set(0.5, 0);
itemText.y = index * 100;
self.addChild(itemText);
// Enable purchase on tap/click
itemText.interactive = true;
itemText.buttonMode = true;
itemText.down = function (x, y, obj) {
var totalPoints = 0;
if (typeof storage !== "undefined") {
totalPoints = storage.totalPoints || 0;
}
if (totalPoints >= item.cost) {
// Deduct cost and update storage
totalPoints -= item.cost;
if (typeof storage !== "undefined") {
storage.totalPoints = totalPoints;
}
updatePointsText();
if (typeof updatePersistentPointsDisplay === "function") {
updatePersistentPointsDisplay();
}
// Show feedback (flash)
LK.effects.flashObject(itemText, 0x00FF00, 400);
// Apply item effect in game
if (item.name === "Extra Life") {
// Give player an extra life (persist for next game)
if (typeof storage !== "undefined") {
var extraLives = storage.extraLives || 0;
storage.extraLives = extraLives + 1;
if (typeof updateLivesDisplay === "function") {
updateLivesDisplay();
} // Update heart icons UI
// updatePointsText(); // This is already called after storage.totalPoints update, which also updates lives text in shop
}
}
if (item.name === "Shield") {
// Give player a shield (persist for next game)
if (typeof storage !== "undefined") {
var shields = storage.shields || 0;
storage.shields = shields + 1;
}
}
// Optionally: disable item after purchase (one-time buy)
// itemText.alpha = 0.5;
// itemText.interactive = false;
} else {
// Not enough points, flash red
LK.effects.flashObject(itemText, 0xFF0000, 400);
}
};
});
return self;
});
/****
* Initialize Game
****/
//
var game = new LK.Game({
backgroundColor: 0x181c2c
});
/****
* Game Code
****/
// Level variables
var currentLevel = 1;
var levelText;
var levelUpInterval = 30 * 60; // Level up every 30 seconds (1800 ticks)
var lastLevelUpTick = 0;
// Show shop on demand (e.g. after game over or via button)
// --- Shop display ---
// Asset for life display
// New asset for Kimchi
function showShop() {
var shop = new Shop();
// Center shop based on its size
shop.x = GAME_W / 2;
shop.y = GAME_H / 2;
shop.anchorX = 0.5;
shop.anchorY = 0.5;
game.addChild(shop);
// Bring shop to top (after all gameplay elements)
if (shop.parent && shop.parent.children) {
shop.parent.removeChild(shop);
shop.parent.addChild(shop);
}
// Remove shop after 5 seconds
LK.setTimeout(function () {
if (shop && shop.parent) {
shop.destroy();
}
}, 5000);
}
// --- Main Menu Overlay ---
var mainMenuOverlay;
function showMainMenu() {
// Prevent duplicate overlays
if (mainMenuOverlay && mainMenuOverlay.parent) return;
mainMenuOverlay = new Container();
// Semi-transparent background
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 30,
scaleY: 40,
x: GAME_W / 2,
y: GAME_H / 2,
tint: 0x000000
});
bg.alpha = 0.7;
mainMenuOverlay.addChild(bg);
// Game Title
var title = new Text2("Ana Menü", {
size: 180,
fill: 0xFFF700
});
title.anchor.set(0.5, 0.5);
title.x = GAME_W / 2;
title.y = GAME_H / 2 - 350;
mainMenuOverlay.addChild(title);
// Start Button
var startBtn = new Text2("Başla", {
size: 140,
fill: 0xFFFFFF
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = GAME_W / 2;
startBtn.y = GAME_H / 2 + 100;
startBtn.interactive = true;
startBtn.buttonMode = true;
startBtn.down = function (x, y, obj) {
if (mainMenuOverlay && mainMenuOverlay.parent) {
mainMenuOverlay.destroy();
}
// Activate gameplay
gameActive = true;
// Reset timer for new session
gameStartTime = LK.ticks;
lastLevelUpTick = LK.ticks; // Initialize level timer with current game ticks
elapsedSeconds = 0;
if (elapsedTimeText) {
elapsedTimeText.setText('Time: 0s');
}
// Reset score and scoreTxt at game start
score = 0;
LK.setScore(0);
if (typeof scoreTxt !== "undefined") {
scoreTxt.setText('0');
}
};
mainMenuOverlay.addChild(startBtn);
// Optionally, add instructions
var info = new Text2("Ekrana tıklayarak ateş et\nYüzünü hareket ettirerek karakteri kontrol et", {
size: 70,
fill: 0xFFFFFF
});
info.anchor.set(0.5, 0.5);
info.x = GAME_W / 2;
info.y = GAME_H / 2 + 300;
mainMenuOverlay.addChild(info);
game.addChild(mainMenuOverlay);
}
// --- Gameplay lock until Başla is pressed ---
var gameActive = false; // Only true after Başla is pressed
// Show main menu at game start
LK.setTimeout(function () {
showMainMenu();
}, 10);
// Timer variables
var elapsedTimeText;
var gameStartTime = 0; // To store LK.ticks at the start of the game
var elapsedSeconds = 0; // To store the calculated elapsed seconds
// Obstacle: red rectangle
// Character: main player controlled by face
// Game area dimensions
var GAME_W = 2048;
var GAME_H = 2732;
// Player character
var faceChar = new FaceChar();
game.addChild(faceChar);
// Gun instance
var gun; // Gun instance
// Create and add the gun
gun = new Gun();
game.addChild(gun);
// Initial position: bottom center, 400px from bottom
faceChar.x = GAME_W / 2;
faceChar.y = GAME_H - 400;
// Arrays for obstacles and points
var obstacles = [];
var points = [];
var bullets = []; // Array to store active bullets
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFF700
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Level Display
levelText = new Text2('Level: 1', {
size: 80,
fill: 0xADD8E6 // A nice light blue color for the level text
});
levelText.anchor.set(0, 0); // Anchor to its own top-left
LK.gui.topLeft.addChild(levelText);
// Position it carefully in the top-left, avoiding the 100x100px menu icon area
levelText.x = 250; // Increased X to move it further right (100px for menu icon + 150px padding)
levelText.y = 20; // 20px from the top edge of the screen
// Persistent Currency ("Para") Display will be initialized by updatePersistentPointsDisplay
// Lives display elements
var livesContainer = new Container();
LK.gui.topRight.addChild(livesContainer); // Add to top-right GUI area
// livesContainer.anchor.set(1, 0); // Removed: Containers do not have an 'anchor' property for self-positioning.
livesContainer.x = -30; // Position 30px from the screen's right edge (relative to gui.topRight)
livesContainer.y = 30; // Position 30px from the screen's top edge (relative to gui.topRight)
var heartIcons = []; // Array to keep track of heart icon objects
function updateLivesDisplay() {
// Clear previously displayed hearts
heartIcons.forEach(function (icon) {
if (icon && icon.parent) {
// Check if icon exists and is attached
icon.destroy();
}
});
heartIcons = []; // Reset the array
var currentLives = 0;
// storage.extraLives is initialized by game logic (e.g., to 3 at game start/reset)
// before this function is typically called.
if (typeof storage !== "undefined" && typeof storage.extraLives === "number") {
currentLives = storage.extraLives;
}
// If storage.extraLives were somehow not a number here, 0 hearts would be displayed,
// accurately reflecting an issue with the life count data, rather than masking it.
// Optionally cap the number of hearts displayed to avoid UI clutter
var displayableLives = Math.min(currentLives, 5); // e.g., display max 5 hearts
for (var i = 0; i < displayableLives; i++) {
var heart = LK.getAsset('heart_icon', {
anchorX: 1,
anchorY: 0.5
}); // Anchor heart's right-middle
if (heart) {
// Position hearts horizontally from right to left within the livesContainer
// The rightmost heart (i=0) is at x=0 relative to container's (anchored) right edge
heart.x = -i * (heart.width + 10); // 10px spacing between hearts
heart.y = heart.height / 2; // Vertically center hearts in the container
livesContainer.addChild(heart);
heartIcons.push(heart);
}
}
}
// For keeping track of score
// Load score from storage if available (after game over)
var score = 0;
if (typeof storage !== "undefined") {
// Use lastScore for current run, totalPoints for shop
var lastScore = storage.lastScore;
if (typeof lastScore === "number" && !isNaN(lastScore)) {
score = lastScore;
LK.setScore(score);
}
// Ensure extraLives, shields and totalPoints are defined
if (typeof storage.extraLives === "undefined" || storage.extraLives === 0) {
storage.extraLives = 3;
}
if (typeof storage.shields === "undefined") {
storage.shields = 0;
}
if (typeof storage.totalPoints === "undefined") {
storage.totalPoints = 0;
} // Ensure totalPoints is initialized
updateLivesDisplay(); // Initialize hearts display
if (typeof updatePersistentPointsDisplay === "function") {
updatePersistentPointsDisplay();
} // Initialize Para display
}
// Elapsed Time Display
elapsedTimeText = new Text2('Time: 0s', {
size: 70,
fill: 0xFFFFFF // White color for the text
});
elapsedTimeText.anchor.set(0, 1); // Anchor to the bottom-left of the text
LK.gui.bottomLeft.addChild(elapsedTimeText);
// Position it 30px from the left and 30px from the bottom of the screen
elapsedTimeText.x = 30;
elapsedTimeText.y = -30;
// Initialize timer variables for the current game session
gameStartTime = LK.ticks;
elapsedSeconds = 0;
elapsedTimeText.setText('Time: ' + elapsedSeconds + 's'); // Set initial text
// For obstacle/point spawn timing
var lastObstacleTick = 0;
var lastPointTick = 0;
var lastShotTick = 0; // Tracks the game tick of the last shot
var fireCooldownTicks = 60; // Minimum ticks between shots (60 ticks = 1 second at 60 FPS)
var initialBulletSpeed = -30; // Base speed for bullets at level 1
var bulletSpeedIncrementPerLevel = 2; // How much faster (more negative) bullets get per level
// For difficulty scaling
var minObstacleInterval = 45; // ticks
var minPointInterval = 60; // ticks
var obstacleSpeed = 12; // px per frame, increases over time
// For collision state
var lastCollided = false;
// For point collection state
var lastPointCollided = {};
// Facekit smoothing
var lastFaceX = faceChar.x;
var lastFaceY = faceChar.y;
// Persistent currency display
var persistentPointsContainer;
var persistentPointsText;
var persistentCoinIcon;
function updatePersistentPointsDisplay() {
var spacing = 15; // Spacing between text and icon
// Ensure container exists and is on GUI
if (!persistentPointsContainer) {
persistentPointsContainer = new Container();
LK.gui.topRight.addChild(persistentPointsContainer); // Attach to top-right
persistentPointsContainer.x = -30; // Position 30px from the screen's right edge
persistentPointsContainer.y = 140; // Position below lives display (lives at y=30, hearts ~70px high)
}
// Ensure Text2 object for points exists
if (!persistentPointsText) {
persistentPointsText = new Text2("Coin: 0", {
size: 70,
fill: 0xFFFF00
});
persistentPointsText.anchor.set(1, 0.5); // Anchor right-middle for positioning
}
// Ensure text is child of container
if (persistentPointsText.parent !== persistentPointsContainer) {
if (persistentPointsText.parent) {
persistentPointsText.parent.removeChild(persistentPointsText);
}
persistentPointsContainer.addChild(persistentPointsText);
}
// Ensure coin icon asset exists
if (!persistentCoinIcon) {
persistentCoinIcon = LK.getAsset('coin_icon', {
// width/height will come from asset definition in Assets section
anchorX: 1,
// Anchor right
anchorY: 0.5 // Anchor middle for vertical alignment with text
});
}
// Ensure icon is child of container
if (persistentCoinIcon.parent !== persistentPointsContainer) {
if (persistentCoinIcon.parent) {
persistentCoinIcon.parent.removeChild(persistentCoinIcon);
}
persistentPointsContainer.addChild(persistentCoinIcon);
}
// Update text content
var currentPoints = 0;
if (typeof storage !== "undefined" && typeof storage.totalPoints === "number") {
currentPoints = storage.totalPoints;
}
persistentPointsText.setText("Coin: " + currentPoints);
// Position text and icon within the container for right alignment.
// The container's top-left is positioned by persistentPointsContainer.x and .y relative to LK.gui.topRight.
// Children x,y are relative to the container's top-left.
// persistentCoinIcon is rightmost. Its anchor (1, 0.5) means its right-middle point is at its x,y.
// We position its right-middle point at (0,0) relative to container's origin (top-left).
persistentCoinIcon.x = 0;
persistentCoinIcon.y = 0; // Vertically centered due to anchorY 0.5 and container's y
// persistentPointsText is to the left. Its anchor (1, 0.5) means its right-middle point is at its x,y.
// We position its right-middle point to be (persistentCoinIcon.width + spacing) to the left of the icon's right-middle.
persistentPointsText.x = -(persistentCoinIcon.width + spacing);
persistentPointsText.y = 0; // Vertically centered due to anchorY 0.5 and container's y
}
// Handle screen tap to fire bullets
game.down = function (x, y, obj) {
if (!gameActive) return; // Prevent firing if game not started
// Check cooldown: If current time (LK.ticks) minus last shot time is less than the cooldown period, do nothing.
if (LK.ticks - lastShotTick < fireCooldownTicks) {
return; // Still in cooldown period, prevent firing
}
if (gun && gun.visual && faceChar) {
// Ensure gun, its visual, and character exist
var currentBulletSpeed = initialBulletSpeed - (currentLevel - 1) * bulletSpeedIncrementPerLevel;
var newBullet = new Bullet(currentBulletSpeed);
newBullet.x = gun.x;
// Gun's anchorY is 1.0 (bottom). gun.y is its base.
// Gun's tip is gun.y - gun.visual.height.
// Bullet's anchorY is 0.5 (center).
newBullet.y = gun.y - gun.visual.height;
// Initialize lastY for off-screen detection logic
newBullet.lastY = newBullet.y;
bullets.push(newBullet);
game.addChild(newBullet);
lastShotTick = LK.ticks; // Update the time of the last shot
// Optional: Play a shooting sound if an asset 'shoot' is available
// if (LK.getSound('shoot')) {
// LK.getSound('shoot').play();
// Optionally, reset other game state here if needed
lastPointCollided = {};
}
;
};
// Main update loop
game.update = function () {
if (!gameActive) return; // Block all gameplay updates until Başla is pressed
// --- Timer Update ---
// Calculate total elapsed seconds since the game started
var currentTotalSeconds = Math.floor((LK.ticks - gameStartTime) / 60);
// Update the text only if the number of seconds has changed
if (currentTotalSeconds !== elapsedSeconds) {
elapsedSeconds = currentTotalSeconds;
if (elapsedTimeText) {
// Check if the text object exists
elapsedTimeText.setText('Time: ' + elapsedSeconds + 's');
}
}
// --- Level Progression ---
if (LK.ticks - lastLevelUpTick > levelUpInterval) {
currentLevel++;
if (levelText) {
levelText.setText('Level: ' + currentLevel);
}
lastLevelUpTick = LK.ticks;
// Increase difficulty with the new level
obstacleSpeed += 1.0; // Make obstacles faster
// Decrease spawn intervals (more frequent spawns), but with a minimum cap
if (minObstacleInterval > 15) {
// Don't let obstacles spawn too frenetically
minObstacleInterval -= 2;
}
if (minPointInterval > 20) {
// Same for points/coins
minPointInterval -= 2;
}
// Optional: Cap the maximum obstacle speed to prevent it from becoming unplayably fast
if (obstacleSpeed > 40) {
obstacleSpeed = 40;
}
// You could add other effects or difficulty adjustments here when a new level is reached!
}
// --- Facekit control ---
// Use mouthCenter for X/Y, clamp to game area
var fx = facekit.mouthCenter && typeof facekit.mouthCenter.x === "number" ? facekit.mouthCenter.x : GAME_W / 2;
var fy = facekit.mouthCenter && typeof facekit.mouthCenter.y === "number" ? facekit.mouthCenter.y : GAME_H - 400;
// Optionally, if mouth is open, move faster (boost)
var boost = facekit.mouthOpen ? 1.7 : 1.0;
// Smooth movement (lerp)
lastFaceX += (fx - lastFaceX) * 0.25 * boost;
lastFaceY += (fy - lastFaceY) * 0.25 * boost;
// Clamp to bounds (keep inside screen)
var charW = faceChar.width;
var charH = faceChar.height;
var minX = charW / 2 + 40;
var maxX = GAME_W - charW / 2 - 40;
var minY = charH / 2 + 120;
var maxY = GAME_H - charH / 2 - 40;
faceChar.x = Math.max(minX, Math.min(maxX, lastFaceX));
faceChar.y = Math.max(minY, Math.min(maxY, lastFaceY));
// --- Spawn obstacles ---
// Update gun position to follow faceChar
if (gun && faceChar) {
gun.x = faceChar.x;
// Position the base of the gun (anchorY = 1.0) at the top edge of faceChar
gun.y = faceChar.y - faceChar.height / 2;
}
// --- Manage Bullets ---
for (var k = bullets.length - 1; k >= 0; k--) {
var bullet = bullets[k];
// Initialize lastY for tracking changes on Y if not already done (e.g. if not set at creation)
if (bullet.lastY === undefined) {
bullet.lastY = bullet.y;
}
// bullet.update(); // Bullet's own update method is called automatically by LK engine if bullet is added to game stage
// Off-Screen Detection (top of screen)
// A bullet's y is its center (anchorY: 0.5).
// It's off-screen if its center y is < -(bullet.visual.height / 2).
var bulletHeight = bullet.visual ? bullet.visual.height : 50; // Fallback height
var offScreenThreshold = -(bulletHeight / 2) - 10; // Add a small buffer beyond the edge
if (bullet.y < offScreenThreshold) {
// More robust check: if it *crossed* the boundary in this frame
if (bullet.lastY >= offScreenThreshold) {
bullet.destroy(); // Destroy can only be called from the main 'Game' class
bullets.splice(k, 1);
continue; // Skip further processing for this bullet
} else if (bullet.y < -GAME_H) {
// Failsafe: if it's way off screen (e.g. due to lag/teleport)
bullet.destroy();
bullets.splice(k, 1);
continue;
}
}
bullet.lastY = bullet.y; // Update last known Y position
// Bullet-Obstacle Collision
// Iterate through obstacles to check for collision with the current bullet
for (var obs_idx = obstacles.length - 1; obs_idx >= 0; obs_idx--) {
var obstacle = obstacles[obs_idx];
// Ensure the bullet still exists and is part of the game scene.
// This check is important because the bullet might have been destroyed by off-screen logic
// or by a previous collision in this same frame if multiple collisions were possible.
if (!bullet || !bullet.parent) {
// If bullet is no longer valid (e.g., already destroyed and removed from its parent container),
// then stop checking it against further obstacles.
break;
}
// Ensure the obstacle also still exists and is part of the game scene.
if (obstacle && obstacle.parent && bullet.intersects(obstacle)) {
// Collision detected!
// Destroy the obstacle
obstacle.destroy();
obstacles.splice(obs_idx, 1);
// Award points for destroying obstacle
score += 5;
LK.setScore(score);
if (scoreTxt) {
scoreTxt.setText(score);
}
// Destroy the bullet
bullet.destroy();
bullets.splice(k, 1); // k is the index from the outer bullets loop
// Since this bullet has been destroyed, break from iterating through more obstacles for it.
// The outer loop (for bullets) will correctly handle the modified bullets array due to the splice.
break;
}
}
// End of Bullet-Obstacle Collision logic for the current bullet
}
// --- Spawn obstacles ---
if (LK.ticks - lastObstacleTick > minObstacleInterval + Math.floor(Math.random() * 40)) {
var obs = new Obstacle();
// Random X, avoid leftmost 100px (menu)
var obsW = obs.width;
var obsX = 100 + obsW / 2 + Math.random() * (GAME_W - obsW - 200);
obs.x = obsX;
obs.y = -obs.height / 2;
obs.speed = obstacleSpeed + Math.random() * 3;
obstacles.push(obs);
game.addChild(obs);
lastObstacleTick = LK.ticks;
}
// --- Spawn points ---
if (LK.ticks - lastPointTick > minPointInterval + Math.floor(Math.random() * 60)) {
var pt = new CoinCollectible();
var ptW = pt.width;
var ptX = 100 + ptW / 2 + Math.random() * (GAME_W - ptW - 200);
pt.x = ptX;
pt.y = -pt.height / 2;
pt.speed = obstacleSpeed * 0.8 + Math.random() * 2;
points.push(pt);
game.addChild(pt);
lastPointTick = LK.ticks;
}
// --- Move obstacles, check collision ---
var collided = false;
for (var i = obstacles.length - 1; i >= 0; i--) {
var o = obstacles[i];
o.update();
// Off screen
if (o.y - o.height / 2 > GAME_H + 100) {
o.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with player
if (faceChar.intersects(o)) {
collided = true;
}
}
// --- Move points, check collection ---
for (var j = points.length - 1; j >= 0; j--) {
var p = points[j];
p.update();
// Off screen
if (p.y - p.height / 2 > GAME_H + 100) {
p.destroy();
points.splice(j, 1);
continue;
}
// Collision with player
var pointId = p._lkid || p._id || j;
if (!lastPointCollided[pointId]) {
if (faceChar.intersects(p)) {
// Collect point
score += 1;
LK.setScore(score);
scoreTxt.setText(score);
// Flash effect
LK.effects.flashObject(p, 0xffffff, 200);
p.destroy();
points.splice(j, 1);
lastPointCollided[pointId] = true;
continue;
}
}
}
// --- Collision state transitions ---
if (!lastCollided && collided) {
// Check for shield first
var usedShield = false;
if (typeof storage !== "undefined" && storage.shields && storage.shields > 0) {
storage.shields = storage.shields - 1;
usedShield = true;
LK.effects.flashScreen(0x00ffff, 600);
faceChar.flash();
// Don't trigger game over, just consume shield
}
// If no shield, check for extra life
else if (typeof storage !== "undefined" && storage.extraLives && storage.extraLives > 0) {
storage.extraLives = storage.extraLives - 1;
if (typeof updateLivesDisplay === "function") {
updateLivesDisplay();
} // Update hearts display
LK.effects.flashScreen(0x00ff00, 600);
faceChar.flash();
// Don't trigger game over, just consume extra life
}
// No shield or extra life, trigger game over
else {
LK.effects.flashScreen(0xff0000, 800);
faceChar.flash();
LK.showGameOver();
return;
}
}
lastCollided = collided;
// --- Difficulty scaling ---
if (LK.ticks % 300 === 0 && obstacleSpeed < 32) {
obstacleSpeed += 1.2;
if (minObstacleInterval > 20) {
minObstacleInterval -= 2;
}
if (minPointInterval > 30) {
minPointInterval -= 2;
}
}
};
// --- Instructions text removed ---
// --- Center score text ---
scoreTxt.x = 0;
scoreTxt.y = 20;
// --- Clean up on game over ---
game.on('destroy', function () {
// Save score to persistent storage before reset
if (typeof storage !== "undefined") {
storage.lastScore = score;
// Only add this run's score to totalPoints if score > 0
if (score > 0) {
var totalPoints = storage.totalPoints || 0;
// Convert 2 game points to 1 Coin
storage.totalPoints = totalPoints + Math.floor(score / 2);
}
if (typeof updatePersistentPointsDisplay === "function") {
updatePersistentPointsDisplay();
}
if (persistentPointsText) {
persistentPointsText.setText("Coin: 0");
}
}
// Destroy all active bullets
for (var i_bullet_cleanup = bullets.length - 1; i_bullet_cleanup >= 0; i_bullet_cleanup--) {
if (bullets[i_bullet_cleanup] && bullets[i_bullet_cleanup].destroy) {
bullets[i_bullet_cleanup].destroy();
}
}
bullets = [];
obstacles = [];
points = [];
lastPointCollided = {};
lastCollided = false;
lastFaceX = GAME_W / 2;
lastFaceY = GAME_H - 400;
if (faceChar) {
faceChar.x = GAME_W / 2;
faceChar.y = GAME_H - 400;
}
score = 0;
LK.setScore(0);
scoreTxt.setText('0');
// Reset level variables
currentLevel = 1;
if (levelText) {
levelText.setText('Level: ' + currentLevel);
levelText.x = 250;
levelText.y = 20;
}
// lastLevelUpTick will be reset correctly when the game starts again via the main menu button.
// Setting to 0 here is a safe default if the game could restart without the menu.
lastLevelUpTick = 0;
elapsedSeconds = 0;
if (elapsedTimeText) {
elapsedTimeText.setText('Time: 0s');
}
if (gun) {
// gun instance is a child of game, so it will be destroyed automatically by LK.
// We just need to nullify the reference.
gun = null;
}
// Recreate gun for new game
gun = new Gun();
game.addChild(gun);
// Reset lives to 3 for new game
if (typeof storage !== "undefined") {
storage.extraLives = 3;
if (typeof updateLivesDisplay === "function") {
updateLivesDisplay();
} // Update hearts display
}
// Show shop after game over
showShop();
// Reset scoreTxt to show 0 for new game
if (typeof scoreTxt !== "undefined") {
scoreTxt.setText('0');
}
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var facekit = LK.import("@upit/facekit.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (initialSpeed) {
var self = Container.call(this);
// Added initialSpeed parameter
//Create and attach asset.
self.visual = self.attachAsset('bullet', {
// Uses the existing 'bullet' image asset
anchorX: 0.5,
anchorY: 0.5
});
self.speed = initialSpeed; // Use the passed initialSpeed for movement
// If this instance of bullet is attached, this method will be called every tick by the LK engine automatically.
self.update = function () {
self.y += self.speed;
};
return self;
});
// Coin collectible class (was PointCollectible)
var CoinCollectible = Container.expand(function () {
var self = Container.call(this);
var coin = self.attachAsset('coin_icon', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player character controlled by face
var FaceChar = Container.expand(function () {
var self = Container.call(this);
var _char = self.attachAsset('faceChar', {
anchorX: 0.5,
anchorY: 0.5
});
// For possible future effects
self.flash = function () {
LK.effects.flashObject(self, 0xffffff, 300);
};
return self;
});
var Gun = Container.expand(function () {
var self = Container.call(this);
// The visual asset for the gun
self.visual = self.attachAsset('gun_visual', {
anchorX: 0.5,
// Anchor to its horizontal center
anchorY: 1.0 // Anchor to its bottom edge (base of the gun)
});
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obs = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
// Speed is set on creation
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Shop class for purchasing items
var Shop = Container.expand(function () {
var self = Container.call(this);
var items = [{
name: "Speed Boost",
cost: 10
}, {
name: "Extra Life",
cost: 20
}, {
name: "Shield",
cost: 15
}];
var shopText = new Text2("Shop", {
size: 100,
fill: 0xFFFFFF
});
shopText.anchor.set(0.5, 0);
self.addChild(shopText);
shopText.y = -200;
// Show current persistent points
var pointsText = new Text2("", {
size: 70,
fill: 0xFFFF00
});
pointsText.anchor.set(0.5, 0);
pointsText.y = -100;
self.addChild(pointsText);
function updatePointsText() {
var totalPoints = 0;
var extraLives = 0;
var shields = 0;
if (typeof storage !== "undefined") {
totalPoints = storage.totalPoints || 0;
extraLives = typeof storage.extraLives !== "undefined" ? storage.extraLives : 3;
shields = storage.shields || 0;
}
pointsText.setText("Points: " + totalPoints + " | Extra Lives: " + extraLives + " | Shields: " + shields);
// Also update global scoreTxt if shop is open
if (typeof scoreTxt !== "undefined") {
scoreTxt.setText(totalPoints);
}
}
updatePointsText();
// Add shop items and purchase logic
items.forEach(function (item, index) {
var itemText = new Text2(item.name + " - " + item.cost + " points", {
size: 70,
fill: 0xFFFFFF
});
itemText.anchor.set(0.5, 0);
itemText.y = index * 100;
self.addChild(itemText);
// Enable purchase on tap/click
itemText.interactive = true;
itemText.buttonMode = true;
itemText.down = function (x, y, obj) {
var totalPoints = 0;
if (typeof storage !== "undefined") {
totalPoints = storage.totalPoints || 0;
}
if (totalPoints >= item.cost) {
// Deduct cost and update storage
totalPoints -= item.cost;
if (typeof storage !== "undefined") {
storage.totalPoints = totalPoints;
}
updatePointsText();
if (typeof updatePersistentPointsDisplay === "function") {
updatePersistentPointsDisplay();
}
// Show feedback (flash)
LK.effects.flashObject(itemText, 0x00FF00, 400);
// Apply item effect in game
if (item.name === "Extra Life") {
// Give player an extra life (persist for next game)
if (typeof storage !== "undefined") {
var extraLives = storage.extraLives || 0;
storage.extraLives = extraLives + 1;
if (typeof updateLivesDisplay === "function") {
updateLivesDisplay();
} // Update heart icons UI
// updatePointsText(); // This is already called after storage.totalPoints update, which also updates lives text in shop
}
}
if (item.name === "Shield") {
// Give player a shield (persist for next game)
if (typeof storage !== "undefined") {
var shields = storage.shields || 0;
storage.shields = shields + 1;
}
}
// Optionally: disable item after purchase (one-time buy)
// itemText.alpha = 0.5;
// itemText.interactive = false;
} else {
// Not enough points, flash red
LK.effects.flashObject(itemText, 0xFF0000, 400);
}
};
});
return self;
});
/****
* Initialize Game
****/
//
var game = new LK.Game({
backgroundColor: 0x181c2c
});
/****
* Game Code
****/
// Level variables
var currentLevel = 1;
var levelText;
var levelUpInterval = 30 * 60; // Level up every 30 seconds (1800 ticks)
var lastLevelUpTick = 0;
// Show shop on demand (e.g. after game over or via button)
// --- Shop display ---
// Asset for life display
// New asset for Kimchi
function showShop() {
var shop = new Shop();
// Center shop based on its size
shop.x = GAME_W / 2;
shop.y = GAME_H / 2;
shop.anchorX = 0.5;
shop.anchorY = 0.5;
game.addChild(shop);
// Bring shop to top (after all gameplay elements)
if (shop.parent && shop.parent.children) {
shop.parent.removeChild(shop);
shop.parent.addChild(shop);
}
// Remove shop after 5 seconds
LK.setTimeout(function () {
if (shop && shop.parent) {
shop.destroy();
}
}, 5000);
}
// --- Main Menu Overlay ---
var mainMenuOverlay;
function showMainMenu() {
// Prevent duplicate overlays
if (mainMenuOverlay && mainMenuOverlay.parent) return;
mainMenuOverlay = new Container();
// Semi-transparent background
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 30,
scaleY: 40,
x: GAME_W / 2,
y: GAME_H / 2,
tint: 0x000000
});
bg.alpha = 0.7;
mainMenuOverlay.addChild(bg);
// Game Title
var title = new Text2("Ana Menü", {
size: 180,
fill: 0xFFF700
});
title.anchor.set(0.5, 0.5);
title.x = GAME_W / 2;
title.y = GAME_H / 2 - 350;
mainMenuOverlay.addChild(title);
// Start Button
var startBtn = new Text2("Başla", {
size: 140,
fill: 0xFFFFFF
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = GAME_W / 2;
startBtn.y = GAME_H / 2 + 100;
startBtn.interactive = true;
startBtn.buttonMode = true;
startBtn.down = function (x, y, obj) {
if (mainMenuOverlay && mainMenuOverlay.parent) {
mainMenuOverlay.destroy();
}
// Activate gameplay
gameActive = true;
// Reset timer for new session
gameStartTime = LK.ticks;
lastLevelUpTick = LK.ticks; // Initialize level timer with current game ticks
elapsedSeconds = 0;
if (elapsedTimeText) {
elapsedTimeText.setText('Time: 0s');
}
// Reset score and scoreTxt at game start
score = 0;
LK.setScore(0);
if (typeof scoreTxt !== "undefined") {
scoreTxt.setText('0');
}
};
mainMenuOverlay.addChild(startBtn);
// Optionally, add instructions
var info = new Text2("Ekrana tıklayarak ateş et\nYüzünü hareket ettirerek karakteri kontrol et", {
size: 70,
fill: 0xFFFFFF
});
info.anchor.set(0.5, 0.5);
info.x = GAME_W / 2;
info.y = GAME_H / 2 + 300;
mainMenuOverlay.addChild(info);
game.addChild(mainMenuOverlay);
}
// --- Gameplay lock until Başla is pressed ---
var gameActive = false; // Only true after Başla is pressed
// Show main menu at game start
LK.setTimeout(function () {
showMainMenu();
}, 10);
// Timer variables
var elapsedTimeText;
var gameStartTime = 0; // To store LK.ticks at the start of the game
var elapsedSeconds = 0; // To store the calculated elapsed seconds
// Obstacle: red rectangle
// Character: main player controlled by face
// Game area dimensions
var GAME_W = 2048;
var GAME_H = 2732;
// Player character
var faceChar = new FaceChar();
game.addChild(faceChar);
// Gun instance
var gun; // Gun instance
// Create and add the gun
gun = new Gun();
game.addChild(gun);
// Initial position: bottom center, 400px from bottom
faceChar.x = GAME_W / 2;
faceChar.y = GAME_H - 400;
// Arrays for obstacles and points
var obstacles = [];
var points = [];
var bullets = []; // Array to store active bullets
// Score display
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFF700
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Level Display
levelText = new Text2('Level: 1', {
size: 80,
fill: 0xADD8E6 // A nice light blue color for the level text
});
levelText.anchor.set(0, 0); // Anchor to its own top-left
LK.gui.topLeft.addChild(levelText);
// Position it carefully in the top-left, avoiding the 100x100px menu icon area
levelText.x = 250; // Increased X to move it further right (100px for menu icon + 150px padding)
levelText.y = 20; // 20px from the top edge of the screen
// Persistent Currency ("Para") Display will be initialized by updatePersistentPointsDisplay
// Lives display elements
var livesContainer = new Container();
LK.gui.topRight.addChild(livesContainer); // Add to top-right GUI area
// livesContainer.anchor.set(1, 0); // Removed: Containers do not have an 'anchor' property for self-positioning.
livesContainer.x = -30; // Position 30px from the screen's right edge (relative to gui.topRight)
livesContainer.y = 30; // Position 30px from the screen's top edge (relative to gui.topRight)
var heartIcons = []; // Array to keep track of heart icon objects
function updateLivesDisplay() {
// Clear previously displayed hearts
heartIcons.forEach(function (icon) {
if (icon && icon.parent) {
// Check if icon exists and is attached
icon.destroy();
}
});
heartIcons = []; // Reset the array
var currentLives = 0;
// storage.extraLives is initialized by game logic (e.g., to 3 at game start/reset)
// before this function is typically called.
if (typeof storage !== "undefined" && typeof storage.extraLives === "number") {
currentLives = storage.extraLives;
}
// If storage.extraLives were somehow not a number here, 0 hearts would be displayed,
// accurately reflecting an issue with the life count data, rather than masking it.
// Optionally cap the number of hearts displayed to avoid UI clutter
var displayableLives = Math.min(currentLives, 5); // e.g., display max 5 hearts
for (var i = 0; i < displayableLives; i++) {
var heart = LK.getAsset('heart_icon', {
anchorX: 1,
anchorY: 0.5
}); // Anchor heart's right-middle
if (heart) {
// Position hearts horizontally from right to left within the livesContainer
// The rightmost heart (i=0) is at x=0 relative to container's (anchored) right edge
heart.x = -i * (heart.width + 10); // 10px spacing between hearts
heart.y = heart.height / 2; // Vertically center hearts in the container
livesContainer.addChild(heart);
heartIcons.push(heart);
}
}
}
// For keeping track of score
// Load score from storage if available (after game over)
var score = 0;
if (typeof storage !== "undefined") {
// Use lastScore for current run, totalPoints for shop
var lastScore = storage.lastScore;
if (typeof lastScore === "number" && !isNaN(lastScore)) {
score = lastScore;
LK.setScore(score);
}
// Ensure extraLives, shields and totalPoints are defined
if (typeof storage.extraLives === "undefined" || storage.extraLives === 0) {
storage.extraLives = 3;
}
if (typeof storage.shields === "undefined") {
storage.shields = 0;
}
if (typeof storage.totalPoints === "undefined") {
storage.totalPoints = 0;
} // Ensure totalPoints is initialized
updateLivesDisplay(); // Initialize hearts display
if (typeof updatePersistentPointsDisplay === "function") {
updatePersistentPointsDisplay();
} // Initialize Para display
}
// Elapsed Time Display
elapsedTimeText = new Text2('Time: 0s', {
size: 70,
fill: 0xFFFFFF // White color for the text
});
elapsedTimeText.anchor.set(0, 1); // Anchor to the bottom-left of the text
LK.gui.bottomLeft.addChild(elapsedTimeText);
// Position it 30px from the left and 30px from the bottom of the screen
elapsedTimeText.x = 30;
elapsedTimeText.y = -30;
// Initialize timer variables for the current game session
gameStartTime = LK.ticks;
elapsedSeconds = 0;
elapsedTimeText.setText('Time: ' + elapsedSeconds + 's'); // Set initial text
// For obstacle/point spawn timing
var lastObstacleTick = 0;
var lastPointTick = 0;
var lastShotTick = 0; // Tracks the game tick of the last shot
var fireCooldownTicks = 60; // Minimum ticks between shots (60 ticks = 1 second at 60 FPS)
var initialBulletSpeed = -30; // Base speed for bullets at level 1
var bulletSpeedIncrementPerLevel = 2; // How much faster (more negative) bullets get per level
// For difficulty scaling
var minObstacleInterval = 45; // ticks
var minPointInterval = 60; // ticks
var obstacleSpeed = 12; // px per frame, increases over time
// For collision state
var lastCollided = false;
// For point collection state
var lastPointCollided = {};
// Facekit smoothing
var lastFaceX = faceChar.x;
var lastFaceY = faceChar.y;
// Persistent currency display
var persistentPointsContainer;
var persistentPointsText;
var persistentCoinIcon;
function updatePersistentPointsDisplay() {
var spacing = 15; // Spacing between text and icon
// Ensure container exists and is on GUI
if (!persistentPointsContainer) {
persistentPointsContainer = new Container();
LK.gui.topRight.addChild(persistentPointsContainer); // Attach to top-right
persistentPointsContainer.x = -30; // Position 30px from the screen's right edge
persistentPointsContainer.y = 140; // Position below lives display (lives at y=30, hearts ~70px high)
}
// Ensure Text2 object for points exists
if (!persistentPointsText) {
persistentPointsText = new Text2("Coin: 0", {
size: 70,
fill: 0xFFFF00
});
persistentPointsText.anchor.set(1, 0.5); // Anchor right-middle for positioning
}
// Ensure text is child of container
if (persistentPointsText.parent !== persistentPointsContainer) {
if (persistentPointsText.parent) {
persistentPointsText.parent.removeChild(persistentPointsText);
}
persistentPointsContainer.addChild(persistentPointsText);
}
// Ensure coin icon asset exists
if (!persistentCoinIcon) {
persistentCoinIcon = LK.getAsset('coin_icon', {
// width/height will come from asset definition in Assets section
anchorX: 1,
// Anchor right
anchorY: 0.5 // Anchor middle for vertical alignment with text
});
}
// Ensure icon is child of container
if (persistentCoinIcon.parent !== persistentPointsContainer) {
if (persistentCoinIcon.parent) {
persistentCoinIcon.parent.removeChild(persistentCoinIcon);
}
persistentPointsContainer.addChild(persistentCoinIcon);
}
// Update text content
var currentPoints = 0;
if (typeof storage !== "undefined" && typeof storage.totalPoints === "number") {
currentPoints = storage.totalPoints;
}
persistentPointsText.setText("Coin: " + currentPoints);
// Position text and icon within the container for right alignment.
// The container's top-left is positioned by persistentPointsContainer.x and .y relative to LK.gui.topRight.
// Children x,y are relative to the container's top-left.
// persistentCoinIcon is rightmost. Its anchor (1, 0.5) means its right-middle point is at its x,y.
// We position its right-middle point at (0,0) relative to container's origin (top-left).
persistentCoinIcon.x = 0;
persistentCoinIcon.y = 0; // Vertically centered due to anchorY 0.5 and container's y
// persistentPointsText is to the left. Its anchor (1, 0.5) means its right-middle point is at its x,y.
// We position its right-middle point to be (persistentCoinIcon.width + spacing) to the left of the icon's right-middle.
persistentPointsText.x = -(persistentCoinIcon.width + spacing);
persistentPointsText.y = 0; // Vertically centered due to anchorY 0.5 and container's y
}
// Handle screen tap to fire bullets
game.down = function (x, y, obj) {
if (!gameActive) return; // Prevent firing if game not started
// Check cooldown: If current time (LK.ticks) minus last shot time is less than the cooldown period, do nothing.
if (LK.ticks - lastShotTick < fireCooldownTicks) {
return; // Still in cooldown period, prevent firing
}
if (gun && gun.visual && faceChar) {
// Ensure gun, its visual, and character exist
var currentBulletSpeed = initialBulletSpeed - (currentLevel - 1) * bulletSpeedIncrementPerLevel;
var newBullet = new Bullet(currentBulletSpeed);
newBullet.x = gun.x;
// Gun's anchorY is 1.0 (bottom). gun.y is its base.
// Gun's tip is gun.y - gun.visual.height.
// Bullet's anchorY is 0.5 (center).
newBullet.y = gun.y - gun.visual.height;
// Initialize lastY for off-screen detection logic
newBullet.lastY = newBullet.y;
bullets.push(newBullet);
game.addChild(newBullet);
lastShotTick = LK.ticks; // Update the time of the last shot
// Optional: Play a shooting sound if an asset 'shoot' is available
// if (LK.getSound('shoot')) {
// LK.getSound('shoot').play();
// Optionally, reset other game state here if needed
lastPointCollided = {};
}
;
};
// Main update loop
game.update = function () {
if (!gameActive) return; // Block all gameplay updates until Başla is pressed
// --- Timer Update ---
// Calculate total elapsed seconds since the game started
var currentTotalSeconds = Math.floor((LK.ticks - gameStartTime) / 60);
// Update the text only if the number of seconds has changed
if (currentTotalSeconds !== elapsedSeconds) {
elapsedSeconds = currentTotalSeconds;
if (elapsedTimeText) {
// Check if the text object exists
elapsedTimeText.setText('Time: ' + elapsedSeconds + 's');
}
}
// --- Level Progression ---
if (LK.ticks - lastLevelUpTick > levelUpInterval) {
currentLevel++;
if (levelText) {
levelText.setText('Level: ' + currentLevel);
}
lastLevelUpTick = LK.ticks;
// Increase difficulty with the new level
obstacleSpeed += 1.0; // Make obstacles faster
// Decrease spawn intervals (more frequent spawns), but with a minimum cap
if (minObstacleInterval > 15) {
// Don't let obstacles spawn too frenetically
minObstacleInterval -= 2;
}
if (minPointInterval > 20) {
// Same for points/coins
minPointInterval -= 2;
}
// Optional: Cap the maximum obstacle speed to prevent it from becoming unplayably fast
if (obstacleSpeed > 40) {
obstacleSpeed = 40;
}
// You could add other effects or difficulty adjustments here when a new level is reached!
}
// --- Facekit control ---
// Use mouthCenter for X/Y, clamp to game area
var fx = facekit.mouthCenter && typeof facekit.mouthCenter.x === "number" ? facekit.mouthCenter.x : GAME_W / 2;
var fy = facekit.mouthCenter && typeof facekit.mouthCenter.y === "number" ? facekit.mouthCenter.y : GAME_H - 400;
// Optionally, if mouth is open, move faster (boost)
var boost = facekit.mouthOpen ? 1.7 : 1.0;
// Smooth movement (lerp)
lastFaceX += (fx - lastFaceX) * 0.25 * boost;
lastFaceY += (fy - lastFaceY) * 0.25 * boost;
// Clamp to bounds (keep inside screen)
var charW = faceChar.width;
var charH = faceChar.height;
var minX = charW / 2 + 40;
var maxX = GAME_W - charW / 2 - 40;
var minY = charH / 2 + 120;
var maxY = GAME_H - charH / 2 - 40;
faceChar.x = Math.max(minX, Math.min(maxX, lastFaceX));
faceChar.y = Math.max(minY, Math.min(maxY, lastFaceY));
// --- Spawn obstacles ---
// Update gun position to follow faceChar
if (gun && faceChar) {
gun.x = faceChar.x;
// Position the base of the gun (anchorY = 1.0) at the top edge of faceChar
gun.y = faceChar.y - faceChar.height / 2;
}
// --- Manage Bullets ---
for (var k = bullets.length - 1; k >= 0; k--) {
var bullet = bullets[k];
// Initialize lastY for tracking changes on Y if not already done (e.g. if not set at creation)
if (bullet.lastY === undefined) {
bullet.lastY = bullet.y;
}
// bullet.update(); // Bullet's own update method is called automatically by LK engine if bullet is added to game stage
// Off-Screen Detection (top of screen)
// A bullet's y is its center (anchorY: 0.5).
// It's off-screen if its center y is < -(bullet.visual.height / 2).
var bulletHeight = bullet.visual ? bullet.visual.height : 50; // Fallback height
var offScreenThreshold = -(bulletHeight / 2) - 10; // Add a small buffer beyond the edge
if (bullet.y < offScreenThreshold) {
// More robust check: if it *crossed* the boundary in this frame
if (bullet.lastY >= offScreenThreshold) {
bullet.destroy(); // Destroy can only be called from the main 'Game' class
bullets.splice(k, 1);
continue; // Skip further processing for this bullet
} else if (bullet.y < -GAME_H) {
// Failsafe: if it's way off screen (e.g. due to lag/teleport)
bullet.destroy();
bullets.splice(k, 1);
continue;
}
}
bullet.lastY = bullet.y; // Update last known Y position
// Bullet-Obstacle Collision
// Iterate through obstacles to check for collision with the current bullet
for (var obs_idx = obstacles.length - 1; obs_idx >= 0; obs_idx--) {
var obstacle = obstacles[obs_idx];
// Ensure the bullet still exists and is part of the game scene.
// This check is important because the bullet might have been destroyed by off-screen logic
// or by a previous collision in this same frame if multiple collisions were possible.
if (!bullet || !bullet.parent) {
// If bullet is no longer valid (e.g., already destroyed and removed from its parent container),
// then stop checking it against further obstacles.
break;
}
// Ensure the obstacle also still exists and is part of the game scene.
if (obstacle && obstacle.parent && bullet.intersects(obstacle)) {
// Collision detected!
// Destroy the obstacle
obstacle.destroy();
obstacles.splice(obs_idx, 1);
// Award points for destroying obstacle
score += 5;
LK.setScore(score);
if (scoreTxt) {
scoreTxt.setText(score);
}
// Destroy the bullet
bullet.destroy();
bullets.splice(k, 1); // k is the index from the outer bullets loop
// Since this bullet has been destroyed, break from iterating through more obstacles for it.
// The outer loop (for bullets) will correctly handle the modified bullets array due to the splice.
break;
}
}
// End of Bullet-Obstacle Collision logic for the current bullet
}
// --- Spawn obstacles ---
if (LK.ticks - lastObstacleTick > minObstacleInterval + Math.floor(Math.random() * 40)) {
var obs = new Obstacle();
// Random X, avoid leftmost 100px (menu)
var obsW = obs.width;
var obsX = 100 + obsW / 2 + Math.random() * (GAME_W - obsW - 200);
obs.x = obsX;
obs.y = -obs.height / 2;
obs.speed = obstacleSpeed + Math.random() * 3;
obstacles.push(obs);
game.addChild(obs);
lastObstacleTick = LK.ticks;
}
// --- Spawn points ---
if (LK.ticks - lastPointTick > minPointInterval + Math.floor(Math.random() * 60)) {
var pt = new CoinCollectible();
var ptW = pt.width;
var ptX = 100 + ptW / 2 + Math.random() * (GAME_W - ptW - 200);
pt.x = ptX;
pt.y = -pt.height / 2;
pt.speed = obstacleSpeed * 0.8 + Math.random() * 2;
points.push(pt);
game.addChild(pt);
lastPointTick = LK.ticks;
}
// --- Move obstacles, check collision ---
var collided = false;
for (var i = obstacles.length - 1; i >= 0; i--) {
var o = obstacles[i];
o.update();
// Off screen
if (o.y - o.height / 2 > GAME_H + 100) {
o.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with player
if (faceChar.intersects(o)) {
collided = true;
}
}
// --- Move points, check collection ---
for (var j = points.length - 1; j >= 0; j--) {
var p = points[j];
p.update();
// Off screen
if (p.y - p.height / 2 > GAME_H + 100) {
p.destroy();
points.splice(j, 1);
continue;
}
// Collision with player
var pointId = p._lkid || p._id || j;
if (!lastPointCollided[pointId]) {
if (faceChar.intersects(p)) {
// Collect point
score += 1;
LK.setScore(score);
scoreTxt.setText(score);
// Flash effect
LK.effects.flashObject(p, 0xffffff, 200);
p.destroy();
points.splice(j, 1);
lastPointCollided[pointId] = true;
continue;
}
}
}
// --- Collision state transitions ---
if (!lastCollided && collided) {
// Check for shield first
var usedShield = false;
if (typeof storage !== "undefined" && storage.shields && storage.shields > 0) {
storage.shields = storage.shields - 1;
usedShield = true;
LK.effects.flashScreen(0x00ffff, 600);
faceChar.flash();
// Don't trigger game over, just consume shield
}
// If no shield, check for extra life
else if (typeof storage !== "undefined" && storage.extraLives && storage.extraLives > 0) {
storage.extraLives = storage.extraLives - 1;
if (typeof updateLivesDisplay === "function") {
updateLivesDisplay();
} // Update hearts display
LK.effects.flashScreen(0x00ff00, 600);
faceChar.flash();
// Don't trigger game over, just consume extra life
}
// No shield or extra life, trigger game over
else {
LK.effects.flashScreen(0xff0000, 800);
faceChar.flash();
LK.showGameOver();
return;
}
}
lastCollided = collided;
// --- Difficulty scaling ---
if (LK.ticks % 300 === 0 && obstacleSpeed < 32) {
obstacleSpeed += 1.2;
if (minObstacleInterval > 20) {
minObstacleInterval -= 2;
}
if (minPointInterval > 30) {
minPointInterval -= 2;
}
}
};
// --- Instructions text removed ---
// --- Center score text ---
scoreTxt.x = 0;
scoreTxt.y = 20;
// --- Clean up on game over ---
game.on('destroy', function () {
// Save score to persistent storage before reset
if (typeof storage !== "undefined") {
storage.lastScore = score;
// Only add this run's score to totalPoints if score > 0
if (score > 0) {
var totalPoints = storage.totalPoints || 0;
// Convert 2 game points to 1 Coin
storage.totalPoints = totalPoints + Math.floor(score / 2);
}
if (typeof updatePersistentPointsDisplay === "function") {
updatePersistentPointsDisplay();
}
if (persistentPointsText) {
persistentPointsText.setText("Coin: 0");
}
}
// Destroy all active bullets
for (var i_bullet_cleanup = bullets.length - 1; i_bullet_cleanup >= 0; i_bullet_cleanup--) {
if (bullets[i_bullet_cleanup] && bullets[i_bullet_cleanup].destroy) {
bullets[i_bullet_cleanup].destroy();
}
}
bullets = [];
obstacles = [];
points = [];
lastPointCollided = {};
lastCollided = false;
lastFaceX = GAME_W / 2;
lastFaceY = GAME_H - 400;
if (faceChar) {
faceChar.x = GAME_W / 2;
faceChar.y = GAME_H - 400;
}
score = 0;
LK.setScore(0);
scoreTxt.setText('0');
// Reset level variables
currentLevel = 1;
if (levelText) {
levelText.setText('Level: ' + currentLevel);
levelText.x = 250;
levelText.y = 20;
}
// lastLevelUpTick will be reset correctly when the game starts again via the main menu button.
// Setting to 0 here is a safe default if the game could restart without the menu.
lastLevelUpTick = 0;
elapsedSeconds = 0;
if (elapsedTimeText) {
elapsedTimeText.setText('Time: 0s');
}
if (gun) {
// gun instance is a child of game, so it will be destroyed automatically by LK.
// We just need to nullify the reference.
gun = null;
}
// Recreate gun for new game
gun = new Gun();
game.addChild(gun);
// Reset lives to 3 for new game
if (typeof storage !== "undefined") {
storage.extraLives = 3;
if (typeof updateLivesDisplay === "function") {
updateLivesDisplay();
} // Update hearts display
}
// Show shop after game over
showShop();
// Reset scoreTxt to show 0 for new game
if (typeof scoreTxt !== "undefined") {
scoreTxt.setText('0');
}
});
A tiny bomb. In-Game asset. 2d. High contrast. No shadows
Gold coin. In-Game asset. 2d. High contrast. No shadows
Mermi. In-Game asset. 2d. High contrast. No shadows
Kırmızı kalp. In-Game asset. 2d. High contrast. No shadows
Silah. In-Game asset. 2d. High contrast. No shadows
faceChar. In-Game asset. 2d. High contrast. No shadows