/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Fog overlay var Fog = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('fog', { anchorX: 0, anchorY: 0, alpha: 0.10 }); self.update = function () { // Animate fog alpha for effect self.asset.alpha = DIFFICULTY.fogAlpha + Math.sin(LK.ticks / 60) * 0.04; }; return self; }); // Galata Tower (goal) var Galata = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('galata', { anchorX: 0.5, anchorY: 1, alpha: 1 }); return self; }); // Ghost base class var Ghost = Container.expand(function () { var self = Container.call(this); self.type = 'cin'; self.speed = 1; self.target = null; self.state = 'idle'; // idle, chasing, jumpscare, stunned self.lastIntersecting = false; self.jumpscarePlayed = false; self.asset = null; self.stunned = false; self.stunTimer = 0; self._originalAlpha = null; self._originalTint = null; self.init = function (type) { self.type = type; var assetId = 'ghost_' + type; self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); self._originalAlpha = self.asset.alpha; self._originalTint = self.asset.tint; }; self.stun = function (duration) { if (!self.stunned) { self.stunned = true; self.stunTimer = duration; self.state = 'stunned'; // Flicker or color change for visual feedback if (self.asset) { self.asset.alpha = 0.45; self.asset.tint = 0x99ccff; } } }; self.update = function () { if (!self.target) return; // Only chase if within range var dx = self.target.x - self.x; var dy = self.target.y - self.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 800) { self.state = 'chasing'; var angle = Math.atan2(dy, dx); self.x += Math.cos(angle) * self.speed; self.y += Math.sin(angle) * self.speed; } else { self.state = 'idle'; } }; return self; }); // Letter collectible var Letter = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('letter', { anchorX: 0.5, anchorY: 0.5 }); self.collected = false; return self; }); // Survivor (player) class var Survivor = Container.expand(function () { var self = Container.call(this); var body = self.attachAsset('survivor', { anchorX: 0.5, anchorY: 0.5 }); self.facing = 1; // 1: right, -1: left // Flashlight beam: cone in front of survivor var beam = self.attachAsset('flashlight_beam', { anchorX: 0.0, // left edge of ellipse at survivor center anchorY: 0.5, x: 60, // offset in front of survivor y: 0, alpha: 0.58, // softer, more visible but still subtle tint: 0xfffbe0 // slightly warmer, less harsh yellow }); beam.zIndex = -1; // behind survivor sprite // Feathered edge effect: animate alpha for a soft pulse, and scale for subtle feathering tween(beam, { alpha: 0.68 }, { duration: 1800, yoyo: true, repeat: Infinity }); tween(beam, { scaleX: 1.12, scaleY: 1.18 }, { duration: 1600, yoyo: true, repeat: Infinity }); self.update = function () { // Update flashlight beam direction and position if (self.facing === 1) { beam.scaleX = 1; beam.x = 60; } else { beam.scaleX = -1; beam.x = -60; } }; return self; }); // Talisman collectible var Talisman = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('talisman', { anchorX: 0.5, anchorY: 0.5 }); self.collected = false; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x07070a }); /**** * Game Code ****/ // Flashlight beam: wide ellipse, semi-transparent yellow // Music // Sound effects // Fog overlay (large, semi-transparent) // Galata Tower (goal) // Letter (collectible) // Talisman (collectible) // Ghosts (cin, karabasan, gulyabani) - different colors // Flashlight beam (ellipse, semi-transparent yellow) // Main character: "Survivor" // Play music // Persistent run counter for progressive difficulty var nightfall_runCount = parseInt(storage.nightfall_runCount || "0", 10); nightfall_runCount++; storage.nightfall_runCount = nightfall_runCount; // Difficulty scaling factors var DIFFICULTY = { run: nightfall_runCount, ghostSpeedMult: 1 + 0.15 * (nightfall_runCount - 1), ghostCountAdd: Math.min(nightfall_runCount - 1, 3), // Increase collectibles per level, minimum 3, up to 6 talismanCount: Math.min(6, 3 + Math.floor((nightfall_runCount - 1) / 1.5)), letterCount: Math.min(8, 3 + Math.floor((nightfall_runCount - 1) / 1.2)), fogAlpha: 0.08 + 0.02 * Math.min(nightfall_runCount - 1, 4), jumpscareVolume: Math.min(1, 0.7 + 0.1 * (nightfall_runCount - 1)), message: nightfall_runCount > 1 ? "They are watching closer this time..." : "" }; // Track total required for win condition var totalTalismansRequired = DIFFICULTY.talismanCount; var totalLettersRequired = DIFFICULTY.letterCount; LK.playMusic('nightfall_bgm', { loop: true, fade: { start: 0, end: 0.22, duration: 2000 } }); // Survivor/player var survivor = new Survivor(); game.addChild(survivor); survivor.x = 400; survivor.y = 2000; // Galata Tower (goal) var galata = new Galata(); game.addChild(galata); galata.x = 2048 / 2; galata.y = 830; // moved down by 70 more pixels // Fog overlay var fog = new Fog(); game.addChild(fog); // Score: talismans and letters collected var talismanScore = 0; var letterScore = 0; // Compact counter UI for collectibles left var collectiblesCounter = new Text2('', { size: 54, fill: 0xFFE066, font: "monospace", alpha: 0.92 }); collectiblesCounter.anchor.set(0.5, 0); // Top-center, horizontally centered collectiblesCounter.x = LK.gui.top.width / 2; collectiblesCounter.y = 40; LK.gui.top.addChild(collectiblesCounter); // Helper to update counter text function updateCollectiblesCounter() { var talismansLeft = Math.max(0, totalTalismansRequired - talismanScore); var lettersLeft = Math.max(0, totalLettersRequired - letterScore); collectiblesCounter.setText("Talismans Left: " + talismansLeft + " | Letters Left: " + lettersLeft); } updateCollectiblesCounter(); // Timer (sunrise in 60 seconds) var timeLeft = 60; var timerTxt = new Text2('01:00', { size: 80, fill: 0xFFB347 }); timerTxt.anchor.set(0.5, 0); LK.gui.top.addChild(timerTxt); timerTxt.y = 180; // Subtle difficulty message var diffMsg = null; if (DIFFICULTY.message) { diffMsg = new Text2(DIFFICULTY.message, { size: 48, fill: 0xFF6666, font: "monospace", alpha: 0.7 }); diffMsg.anchor.set(0.5, 0); diffMsg.x = LK.gui.top.width / 2; diffMsg.y = 260; LK.gui.top.addChild(diffMsg); // Fade out after a few seconds tween(diffMsg, { alpha: 0 }, { duration: 4000, onFinish: function onFinish() { diffMsg.destroy(); } }); } // Place collectibles var talismans = []; var letters = []; for (var i = 0; i < DIFFICULTY.talismanCount; i++) { var t = new Talisman(); // Spread talismans more widely, avoid clustering t.x = 180 + Math.random() * 1680; t.y = 400 + Math.random() * 2100; talismans.push(t); game.addChild(t); } for (var i = 0; i < DIFFICULTY.letterCount; i++) { var l = new Letter(); // Spread letters more widely, avoid clustering l.x = 120 + Math.random() * 1800; l.y = 350 + Math.random() * 2200; letters.push(l); game.addChild(l); } // Place ghosts var ghosts = []; function spawnGhost(type, x, y, speed) { var g = new Ghost(); g.init(type); g.x = x; g.y = y; g.speed = speed; g.target = survivor; ghosts.push(g); game.addChild(g); } // Base ghosts (now much slower for easier gameplay) spawnGhost('cin', 1800, 2200, 0.4 * DIFFICULTY.ghostSpeedMult); spawnGhost('karabasan', 200, 800, 0.28 * DIFFICULTY.ghostSpeedMult); // Fewer extra ghosts for challenge // (Removed one of each type from the original set) spawnGhost('gulyabani', 1800, 600, 0.18 * DIFFICULTY.ghostSpeedMult); // Progressive extra ghosts (reduce to half as many, much slower base speed) for (var i = 0; i < Math.floor(DIFFICULTY.ghostCountAdd / 2); i++) { var types = ['cin', 'karabasan', 'gulyabani']; var type = types[i % types.length]; var safe = false; var x, y; var attempts = 0; while (!safe && attempts < 20) { x = 200 + Math.random() * 1600; y = 600 + Math.random() * 1800; var dx = x - survivor.x; var dy = y - survivor.y; var dist = Math.sqrt(dx * dx + dy * dy); // Enforce a minimum 200px safe radius from the player for enemy spawn if (dist >= 200) safe = true; attempts++; } var baseSpeed = type === 'cin' ? 0.4 : type === 'karabasan' ? 0.28 : 0.18; spawnGhost(type, x, y, baseSpeed * (1.1 + 0.08 * i) * DIFFICULTY.ghostSpeedMult); } // Dragging logic var dragNode = null; function handleMove(x, y, obj) { if (dragNode) { // Clamp to game area var nx = Math.max(60, Math.min(2048 - 60, x)); var ny = Math.max(200, Math.min(2732 - 60, y)); // Determine direction before moving if (nx > survivor.x) survivor.facing = 1;else if (nx < survivor.x) survivor.facing = -1; dragNode.x = nx; dragNode.y = ny; // Flip sprite based on facing if (survivor.facing === -1) { survivor.children[0].scaleX = -1; } else { survivor.children[0].scaleX = 1; } // Instantly update flashlight beam direction if (typeof survivor.update === "function") survivor.update(); } } game.move = handleMove; game.down = function (x, y, obj) { // Only allow dragging survivor if (x > 100 || y > 100) { // avoid top left menu dragNode = survivor; handleMove(x, y, obj); } }; game.up = function (x, y, obj) { dragNode = null; }; // Jumpscare state var jumpscareActive = false; var jumpscareTimer = 0; // Game update game.update = function () { // Update survivor survivor.update(); // Update fog fog.update(); // Update ghosts for (var i = 0; i < ghosts.length; i++) { var g = ghosts[i]; g.update(); // Jumpscare: if ghost intersects survivor var intersecting = g.intersects(survivor); if (!g.lastIntersecting && intersecting && !jumpscareActive) { jumpscareActive = true; jumpscareTimer = 0; LK.effects.flashScreen(0xffffff, 400 + 100 * (DIFFICULTY.run - 1)); var jumpscareSound = LK.getSound('jumpscare'); if (jumpscareSound) jumpscareSound.setVolume ? jumpscareSound.setVolume(DIFFICULTY.jumpscareVolume) : null; LK.getSound('jumpscare').play(); tween(g.asset, { alpha: 1 }, { duration: 200, onFinish: function onFinish() {} }); // Extra visual effect for high difficulty if (DIFFICULTY.run > 2) { LK.effects.flashScreen(0xff0000, 200); } } g.lastIntersecting = intersecting; } // Jumpscare effect if (jumpscareActive) { jumpscareTimer++; // On first frame of jumpscare, show full-screen jumpscare image and start effects if (jumpscareTimer === 1) { // Freeze player input dragNode = null; // Add full-screen jumpscare image (random ghost close-up) var jumpscareGhosts = ['ghost_karabasan', 'ghost_cin', 'ghost_gulyabani']; var jumpscareId = jumpscareGhosts[Math.floor(Math.random() * jumpscareGhosts.length)]; var jumpscareImg = LK.getAsset(jumpscareId, { anchorX: 0.5, anchorY: 0.5, scaleX: 4.5, scaleY: 4.5, x: 2048 / 2, y: 2732 / 2, alpha: 0.98 }); jumpscareImg.zIndex = 9999; game.addChild(jumpscareImg); // (Removed glowing eyes effect) var eyes = []; // Screen shake/glitch effect var shakeTimer = 0; var shakeInterval = LK.setInterval(function () { shakeTimer++; var shakeAmount = 32 - Math.floor(shakeTimer / 2); if (shakeAmount < 2) shakeAmount = 2; game.x = (Math.random() - 0.5) * shakeAmount; game.y = (Math.random() - 0.5) * shakeAmount; // Glitch: randomize alpha and scale jumpscareImg.alpha = 0.93 + Math.random() * 0.07; jumpscareImg.scaleX = 4.3 + Math.random() * 0.4; jumpscareImg.scaleY = 4.3 + Math.random() * 0.4; for (var e = 0; e < eyes.length; e++) { eyes[e].alpha = 0.7 + Math.random() * 0.3; } if (shakeTimer > 30) { // ~0.5s LK.clearInterval(shakeInterval); game.x = 0; game.y = 0; // Fade out jumpscare image and eyes tween(jumpscareImg, { alpha: 0 }, { duration: 400, onFinish: function onFinish() { jumpscareImg.destroy(); for (var e = 0; e < eyes.length; e++) eyes[e].destroy(); } }); } }, 16); // After jumpscare, show dark "You Died" overlay and options LK.setTimeout(function () { // Overlay var overlay = new Container(); var bg = LK.getAsset('fog', { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: 1, scaleY: 1, alpha: 0.92, tint: 0x000000 }); overlay.addChild(bg); // "You Died" text var diedTxt = new Text2('YOU DIED', { size: 220, fill: 0xFF2222, font: "monospace", alpha: 0.93 }); diedTxt.anchor.set(0.5, 0.5); diedTxt.x = 2048 / 2; diedTxt.y = 1100; overlay.addChild(diedTxt); // Subtle ambient message var msgTxt = new Text2('The night is endless...', { size: 70, fill: 0xcccccc, font: "monospace", alpha: 0.7 }); msgTxt.anchor.set(0.5, 0.5); msgTxt.x = 2048 / 2; msgTxt.y = 1400; overlay.addChild(msgTxt); // Retry button var retryBtn = new Text2('Retry from last chapter', { size: 90, fill: 0xFFB347, font: "monospace", alpha: 0.95 }); retryBtn.anchor.set(0.5, 0.5); retryBtn.x = 2048 / 2; retryBtn.y = 1700; overlay.addChild(retryBtn); // Main menu button var menuBtn = new Text2('Return to main menu', { size: 80, fill: 0xcccccc, font: "monospace", alpha: 0.85 }); menuBtn.anchor.set(0.5, 0.5); menuBtn.x = 2048 / 2; menuBtn.y = 1850; overlay.addChild(menuBtn); // Add overlay to game overlay.zIndex = 10001; game.addChild(overlay); // Faint ambient noise (reuse jumpscare sound at low volume) var jumpscareSound = LK.getSound('jumpscare'); if (jumpscareSound && jumpscareSound.setVolume) { jumpscareSound.setVolume(0.12); jumpscareSound.play(); } // Button logic retryBtn.interactive = true; retryBtn.down = function () { // Remove overlay and restart game overlay.destroy(); LK.showGameOver(); // Triggers game reset }; menuBtn.interactive = true; menuBtn.down = function () { overlay.destroy(); LK.showGameOver(); // Let LK engine handle main menu }; }, 900); } return; } // Collect talismans for (var i = talismans.length - 1; i >= 0; i--) { var t = talismans[i]; if (!t.collected && survivor.intersects(t)) { t.collected = true; LK.getSound('talisman').play(); talismanScore++; updateCollectiblesCounter(); tween(t, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { t.destroy(); } }); talismans.splice(i, 1); } } // Collect letters for (var i = letters.length - 1; i >= 0; i--) { var l = letters[i]; if (!l.collected && survivor.intersects(l)) { l.collected = true; LK.getSound('collect').play(); letterScore++; updateCollectiblesCounter(); tween(l, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { l.destroy(); } }); letters.splice(i, 1); } } // Win condition: reach Galata Tower if (survivor.intersects(galata)) { var talismansLeft = totalTalismansRequired - talismanScore; var lettersLeft = totalLettersRequired - letterScore; if (talismansLeft <= 0 && lettersLeft <= 0) { LK.effects.flashScreen(0x00ffcc, 800); LK.showYouWin(); } else { // Block entry and show message if (!game._entryBlockedMsg || game._entryBlockedMsg.destroyed) { game._entryBlockedMsg = new Text2("You must collect all letters and talismans before entering.", { size: 64, fill: 0xFF2222, font: "monospace", alpha: 0.96 }); game._entryBlockedMsg.anchor.set(0.5, 0); game._entryBlockedMsg.x = 2048 / 2; game._entryBlockedMsg.y = galata.y + 120; game.addChild(game._entryBlockedMsg); tween(game._entryBlockedMsg, { alpha: 0 }, { duration: 2200, onFinish: function onFinish() { if (game._entryBlockedMsg) game._entryBlockedMsg.destroy(); } }); } } } // --- Play subtle background sound effect at random intervals (5-10s) --- var backgroundSoundTimer = null; function playBackgroundSoundLoop() { // Play the background sound effect at low volume var bgSound = LK.getSound('background'); if (bgSound && bgSound.setVolume) { bgSound.setVolume(0.18); // subtle, blends with ambient } if (bgSound) { bgSound.play(); } // Schedule next play at a random interval between 5-10 seconds var nextDelay = 5000 + Math.floor(Math.random() * 5000); // ms backgroundSoundTimer = LK.setTimeout(playBackgroundSoundLoop, nextDelay); } // Start the background sound loop playBackgroundSoundLoop(); // Timer if (LK.ticks % 60 === 0 && timeLeft > 0 && !jumpscareActive) { timeLeft--; var min = Math.floor(timeLeft / 60); var sec = timeLeft % 60; timerTxt.setText((min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec); if (timeLeft === 0) { // Time up: game over LK.effects.flashScreen(0x000000, 1200); LK.setTimeout(function () { LK.showGameOver(); }, 1200); } } }; // Center GUI elements timerTxt.x = LK.gui.top.width / 2;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Fog overlay
var Fog = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('fog', {
anchorX: 0,
anchorY: 0,
alpha: 0.10
});
self.update = function () {
// Animate fog alpha for effect
self.asset.alpha = DIFFICULTY.fogAlpha + Math.sin(LK.ticks / 60) * 0.04;
};
return self;
});
// Galata Tower (goal)
var Galata = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('galata', {
anchorX: 0.5,
anchorY: 1,
alpha: 1
});
return self;
});
// Ghost base class
var Ghost = Container.expand(function () {
var self = Container.call(this);
self.type = 'cin';
self.speed = 1;
self.target = null;
self.state = 'idle'; // idle, chasing, jumpscare, stunned
self.lastIntersecting = false;
self.jumpscarePlayed = false;
self.asset = null;
self.stunned = false;
self.stunTimer = 0;
self._originalAlpha = null;
self._originalTint = null;
self.init = function (type) {
self.type = type;
var assetId = 'ghost_' + type;
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
self._originalAlpha = self.asset.alpha;
self._originalTint = self.asset.tint;
};
self.stun = function (duration) {
if (!self.stunned) {
self.stunned = true;
self.stunTimer = duration;
self.state = 'stunned';
// Flicker or color change for visual feedback
if (self.asset) {
self.asset.alpha = 0.45;
self.asset.tint = 0x99ccff;
}
}
};
self.update = function () {
if (!self.target) return;
// Only chase if within range
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 800) {
self.state = 'chasing';
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
} else {
self.state = 'idle';
}
};
return self;
});
// Letter collectible
var Letter = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('letter', {
anchorX: 0.5,
anchorY: 0.5
});
self.collected = false;
return self;
});
// Survivor (player) class
var Survivor = Container.expand(function () {
var self = Container.call(this);
var body = self.attachAsset('survivor', {
anchorX: 0.5,
anchorY: 0.5
});
self.facing = 1; // 1: right, -1: left
// Flashlight beam: cone in front of survivor
var beam = self.attachAsset('flashlight_beam', {
anchorX: 0.0,
// left edge of ellipse at survivor center
anchorY: 0.5,
x: 60,
// offset in front of survivor
y: 0,
alpha: 0.58,
// softer, more visible but still subtle
tint: 0xfffbe0 // slightly warmer, less harsh yellow
});
beam.zIndex = -1; // behind survivor sprite
// Feathered edge effect: animate alpha for a soft pulse, and scale for subtle feathering
tween(beam, {
alpha: 0.68
}, {
duration: 1800,
yoyo: true,
repeat: Infinity
});
tween(beam, {
scaleX: 1.12,
scaleY: 1.18
}, {
duration: 1600,
yoyo: true,
repeat: Infinity
});
self.update = function () {
// Update flashlight beam direction and position
if (self.facing === 1) {
beam.scaleX = 1;
beam.x = 60;
} else {
beam.scaleX = -1;
beam.x = -60;
}
};
return self;
});
// Talisman collectible
var Talisman = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('talisman', {
anchorX: 0.5,
anchorY: 0.5
});
self.collected = false;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x07070a
});
/****
* Game Code
****/
// Flashlight beam: wide ellipse, semi-transparent yellow
// Music
// Sound effects
// Fog overlay (large, semi-transparent)
// Galata Tower (goal)
// Letter (collectible)
// Talisman (collectible)
// Ghosts (cin, karabasan, gulyabani) - different colors
// Flashlight beam (ellipse, semi-transparent yellow)
// Main character: "Survivor"
// Play music
// Persistent run counter for progressive difficulty
var nightfall_runCount = parseInt(storage.nightfall_runCount || "0", 10);
nightfall_runCount++;
storage.nightfall_runCount = nightfall_runCount;
// Difficulty scaling factors
var DIFFICULTY = {
run: nightfall_runCount,
ghostSpeedMult: 1 + 0.15 * (nightfall_runCount - 1),
ghostCountAdd: Math.min(nightfall_runCount - 1, 3),
// Increase collectibles per level, minimum 3, up to 6
talismanCount: Math.min(6, 3 + Math.floor((nightfall_runCount - 1) / 1.5)),
letterCount: Math.min(8, 3 + Math.floor((nightfall_runCount - 1) / 1.2)),
fogAlpha: 0.08 + 0.02 * Math.min(nightfall_runCount - 1, 4),
jumpscareVolume: Math.min(1, 0.7 + 0.1 * (nightfall_runCount - 1)),
message: nightfall_runCount > 1 ? "They are watching closer this time..." : ""
};
// Track total required for win condition
var totalTalismansRequired = DIFFICULTY.talismanCount;
var totalLettersRequired = DIFFICULTY.letterCount;
LK.playMusic('nightfall_bgm', {
loop: true,
fade: {
start: 0,
end: 0.22,
duration: 2000
}
});
// Survivor/player
var survivor = new Survivor();
game.addChild(survivor);
survivor.x = 400;
survivor.y = 2000;
// Galata Tower (goal)
var galata = new Galata();
game.addChild(galata);
galata.x = 2048 / 2;
galata.y = 830; // moved down by 70 more pixels
// Fog overlay
var fog = new Fog();
game.addChild(fog);
// Score: talismans and letters collected
var talismanScore = 0;
var letterScore = 0;
// Compact counter UI for collectibles left
var collectiblesCounter = new Text2('', {
size: 54,
fill: 0xFFE066,
font: "monospace",
alpha: 0.92
});
collectiblesCounter.anchor.set(0.5, 0); // Top-center, horizontally centered
collectiblesCounter.x = LK.gui.top.width / 2;
collectiblesCounter.y = 40;
LK.gui.top.addChild(collectiblesCounter);
// Helper to update counter text
function updateCollectiblesCounter() {
var talismansLeft = Math.max(0, totalTalismansRequired - talismanScore);
var lettersLeft = Math.max(0, totalLettersRequired - letterScore);
collectiblesCounter.setText("Talismans Left: " + talismansLeft + " | Letters Left: " + lettersLeft);
}
updateCollectiblesCounter();
// Timer (sunrise in 60 seconds)
var timeLeft = 60;
var timerTxt = new Text2('01:00', {
size: 80,
fill: 0xFFB347
});
timerTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(timerTxt);
timerTxt.y = 180;
// Subtle difficulty message
var diffMsg = null;
if (DIFFICULTY.message) {
diffMsg = new Text2(DIFFICULTY.message, {
size: 48,
fill: 0xFF6666,
font: "monospace",
alpha: 0.7
});
diffMsg.anchor.set(0.5, 0);
diffMsg.x = LK.gui.top.width / 2;
diffMsg.y = 260;
LK.gui.top.addChild(diffMsg);
// Fade out after a few seconds
tween(diffMsg, {
alpha: 0
}, {
duration: 4000,
onFinish: function onFinish() {
diffMsg.destroy();
}
});
}
// Place collectibles
var talismans = [];
var letters = [];
for (var i = 0; i < DIFFICULTY.talismanCount; i++) {
var t = new Talisman();
// Spread talismans more widely, avoid clustering
t.x = 180 + Math.random() * 1680;
t.y = 400 + Math.random() * 2100;
talismans.push(t);
game.addChild(t);
}
for (var i = 0; i < DIFFICULTY.letterCount; i++) {
var l = new Letter();
// Spread letters more widely, avoid clustering
l.x = 120 + Math.random() * 1800;
l.y = 350 + Math.random() * 2200;
letters.push(l);
game.addChild(l);
}
// Place ghosts
var ghosts = [];
function spawnGhost(type, x, y, speed) {
var g = new Ghost();
g.init(type);
g.x = x;
g.y = y;
g.speed = speed;
g.target = survivor;
ghosts.push(g);
game.addChild(g);
}
// Base ghosts (now much slower for easier gameplay)
spawnGhost('cin', 1800, 2200, 0.4 * DIFFICULTY.ghostSpeedMult);
spawnGhost('karabasan', 200, 800, 0.28 * DIFFICULTY.ghostSpeedMult);
// Fewer extra ghosts for challenge
// (Removed one of each type from the original set)
spawnGhost('gulyabani', 1800, 600, 0.18 * DIFFICULTY.ghostSpeedMult);
// Progressive extra ghosts (reduce to half as many, much slower base speed)
for (var i = 0; i < Math.floor(DIFFICULTY.ghostCountAdd / 2); i++) {
var types = ['cin', 'karabasan', 'gulyabani'];
var type = types[i % types.length];
var safe = false;
var x, y;
var attempts = 0;
while (!safe && attempts < 20) {
x = 200 + Math.random() * 1600;
y = 600 + Math.random() * 1800;
var dx = x - survivor.x;
var dy = y - survivor.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// Enforce a minimum 200px safe radius from the player for enemy spawn
if (dist >= 200) safe = true;
attempts++;
}
var baseSpeed = type === 'cin' ? 0.4 : type === 'karabasan' ? 0.28 : 0.18;
spawnGhost(type, x, y, baseSpeed * (1.1 + 0.08 * i) * DIFFICULTY.ghostSpeedMult);
}
// Dragging logic
var dragNode = null;
function handleMove(x, y, obj) {
if (dragNode) {
// Clamp to game area
var nx = Math.max(60, Math.min(2048 - 60, x));
var ny = Math.max(200, Math.min(2732 - 60, y));
// Determine direction before moving
if (nx > survivor.x) survivor.facing = 1;else if (nx < survivor.x) survivor.facing = -1;
dragNode.x = nx;
dragNode.y = ny;
// Flip sprite based on facing
if (survivor.facing === -1) {
survivor.children[0].scaleX = -1;
} else {
survivor.children[0].scaleX = 1;
}
// Instantly update flashlight beam direction
if (typeof survivor.update === "function") survivor.update();
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
// Only allow dragging survivor
if (x > 100 || y > 100) {
// avoid top left menu
dragNode = survivor;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// Jumpscare state
var jumpscareActive = false;
var jumpscareTimer = 0;
// Game update
game.update = function () {
// Update survivor
survivor.update();
// Update fog
fog.update();
// Update ghosts
for (var i = 0; i < ghosts.length; i++) {
var g = ghosts[i];
g.update();
// Jumpscare: if ghost intersects survivor
var intersecting = g.intersects(survivor);
if (!g.lastIntersecting && intersecting && !jumpscareActive) {
jumpscareActive = true;
jumpscareTimer = 0;
LK.effects.flashScreen(0xffffff, 400 + 100 * (DIFFICULTY.run - 1));
var jumpscareSound = LK.getSound('jumpscare');
if (jumpscareSound) jumpscareSound.setVolume ? jumpscareSound.setVolume(DIFFICULTY.jumpscareVolume) : null;
LK.getSound('jumpscare').play();
tween(g.asset, {
alpha: 1
}, {
duration: 200,
onFinish: function onFinish() {}
});
// Extra visual effect for high difficulty
if (DIFFICULTY.run > 2) {
LK.effects.flashScreen(0xff0000, 200);
}
}
g.lastIntersecting = intersecting;
}
// Jumpscare effect
if (jumpscareActive) {
jumpscareTimer++;
// On first frame of jumpscare, show full-screen jumpscare image and start effects
if (jumpscareTimer === 1) {
// Freeze player input
dragNode = null;
// Add full-screen jumpscare image (random ghost close-up)
var jumpscareGhosts = ['ghost_karabasan', 'ghost_cin', 'ghost_gulyabani'];
var jumpscareId = jumpscareGhosts[Math.floor(Math.random() * jumpscareGhosts.length)];
var jumpscareImg = LK.getAsset(jumpscareId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4.5,
scaleY: 4.5,
x: 2048 / 2,
y: 2732 / 2,
alpha: 0.98
});
jumpscareImg.zIndex = 9999;
game.addChild(jumpscareImg);
// (Removed glowing eyes effect)
var eyes = [];
// Screen shake/glitch effect
var shakeTimer = 0;
var shakeInterval = LK.setInterval(function () {
shakeTimer++;
var shakeAmount = 32 - Math.floor(shakeTimer / 2);
if (shakeAmount < 2) shakeAmount = 2;
game.x = (Math.random() - 0.5) * shakeAmount;
game.y = (Math.random() - 0.5) * shakeAmount;
// Glitch: randomize alpha and scale
jumpscareImg.alpha = 0.93 + Math.random() * 0.07;
jumpscareImg.scaleX = 4.3 + Math.random() * 0.4;
jumpscareImg.scaleY = 4.3 + Math.random() * 0.4;
for (var e = 0; e < eyes.length; e++) {
eyes[e].alpha = 0.7 + Math.random() * 0.3;
}
if (shakeTimer > 30) {
// ~0.5s
LK.clearInterval(shakeInterval);
game.x = 0;
game.y = 0;
// Fade out jumpscare image and eyes
tween(jumpscareImg, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
jumpscareImg.destroy();
for (var e = 0; e < eyes.length; e++) eyes[e].destroy();
}
});
}
}, 16);
// After jumpscare, show dark "You Died" overlay and options
LK.setTimeout(function () {
// Overlay
var overlay = new Container();
var bg = LK.getAsset('fog', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: 1,
scaleY: 1,
alpha: 0.92,
tint: 0x000000
});
overlay.addChild(bg);
// "You Died" text
var diedTxt = new Text2('YOU DIED', {
size: 220,
fill: 0xFF2222,
font: "monospace",
alpha: 0.93
});
diedTxt.anchor.set(0.5, 0.5);
diedTxt.x = 2048 / 2;
diedTxt.y = 1100;
overlay.addChild(diedTxt);
// Subtle ambient message
var msgTxt = new Text2('The night is endless...', {
size: 70,
fill: 0xcccccc,
font: "monospace",
alpha: 0.7
});
msgTxt.anchor.set(0.5, 0.5);
msgTxt.x = 2048 / 2;
msgTxt.y = 1400;
overlay.addChild(msgTxt);
// Retry button
var retryBtn = new Text2('Retry from last chapter', {
size: 90,
fill: 0xFFB347,
font: "monospace",
alpha: 0.95
});
retryBtn.anchor.set(0.5, 0.5);
retryBtn.x = 2048 / 2;
retryBtn.y = 1700;
overlay.addChild(retryBtn);
// Main menu button
var menuBtn = new Text2('Return to main menu', {
size: 80,
fill: 0xcccccc,
font: "monospace",
alpha: 0.85
});
menuBtn.anchor.set(0.5, 0.5);
menuBtn.x = 2048 / 2;
menuBtn.y = 1850;
overlay.addChild(menuBtn);
// Add overlay to game
overlay.zIndex = 10001;
game.addChild(overlay);
// Faint ambient noise (reuse jumpscare sound at low volume)
var jumpscareSound = LK.getSound('jumpscare');
if (jumpscareSound && jumpscareSound.setVolume) {
jumpscareSound.setVolume(0.12);
jumpscareSound.play();
}
// Button logic
retryBtn.interactive = true;
retryBtn.down = function () {
// Remove overlay and restart game
overlay.destroy();
LK.showGameOver(); // Triggers game reset
};
menuBtn.interactive = true;
menuBtn.down = function () {
overlay.destroy();
LK.showGameOver(); // Let LK engine handle main menu
};
}, 900);
}
return;
}
// Collect talismans
for (var i = talismans.length - 1; i >= 0; i--) {
var t = talismans[i];
if (!t.collected && survivor.intersects(t)) {
t.collected = true;
LK.getSound('talisman').play();
talismanScore++;
updateCollectiblesCounter();
tween(t, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
t.destroy();
}
});
talismans.splice(i, 1);
}
}
// Collect letters
for (var i = letters.length - 1; i >= 0; i--) {
var l = letters[i];
if (!l.collected && survivor.intersects(l)) {
l.collected = true;
LK.getSound('collect').play();
letterScore++;
updateCollectiblesCounter();
tween(l, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
l.destroy();
}
});
letters.splice(i, 1);
}
}
// Win condition: reach Galata Tower
if (survivor.intersects(galata)) {
var talismansLeft = totalTalismansRequired - talismanScore;
var lettersLeft = totalLettersRequired - letterScore;
if (talismansLeft <= 0 && lettersLeft <= 0) {
LK.effects.flashScreen(0x00ffcc, 800);
LK.showYouWin();
} else {
// Block entry and show message
if (!game._entryBlockedMsg || game._entryBlockedMsg.destroyed) {
game._entryBlockedMsg = new Text2("You must collect all letters and talismans before entering.", {
size: 64,
fill: 0xFF2222,
font: "monospace",
alpha: 0.96
});
game._entryBlockedMsg.anchor.set(0.5, 0);
game._entryBlockedMsg.x = 2048 / 2;
game._entryBlockedMsg.y = galata.y + 120;
game.addChild(game._entryBlockedMsg);
tween(game._entryBlockedMsg, {
alpha: 0
}, {
duration: 2200,
onFinish: function onFinish() {
if (game._entryBlockedMsg) game._entryBlockedMsg.destroy();
}
});
}
}
}
// --- Play subtle background sound effect at random intervals (5-10s) ---
var backgroundSoundTimer = null;
function playBackgroundSoundLoop() {
// Play the background sound effect at low volume
var bgSound = LK.getSound('background');
if (bgSound && bgSound.setVolume) {
bgSound.setVolume(0.18); // subtle, blends with ambient
}
if (bgSound) {
bgSound.play();
}
// Schedule next play at a random interval between 5-10 seconds
var nextDelay = 5000 + Math.floor(Math.random() * 5000); // ms
backgroundSoundTimer = LK.setTimeout(playBackgroundSoundLoop, nextDelay);
}
// Start the background sound loop
playBackgroundSoundLoop();
// Timer
if (LK.ticks % 60 === 0 && timeLeft > 0 && !jumpscareActive) {
timeLeft--;
var min = Math.floor(timeLeft / 60);
var sec = timeLeft % 60;
timerTxt.setText((min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec);
if (timeLeft === 0) {
// Time up: game over
LK.effects.flashScreen(0x000000, 1200);
LK.setTimeout(function () {
LK.showGameOver();
}, 1200);
}
}
};
// Center GUI elements
timerTxt.x = LK.gui.top.width / 2;
Create a pixel-art monster character inspired by Turkish folklore, specifically "Gulyabani" – a tall, lanky, ghostly creature with pale, bluish-gray skin, long tangled hair covering parts of its face, and glowing red eyes. It should wear torn Ottoman-style rags, with long sharp fingers and a hunched back. Its movement should look floaty and eerie. Style should match 2D retro horror games. Transparent background PNG, facing left side, idle stance.. In-Game asset. 2d. High contrast. No shadows
Create a pixel-art character sprite of "Karabasan" – a dark, shadowy figure from Turkish folklore known to sit on people’s chests during sleep. The character should be faceless or have a barely visible distorted face with empty glowing white eyes. Its body should appear as a mass of smoky darkness, loosely humanoid, with long arms, claw-like hands, and ragged shadowy edges. It should look like it’s floating slightly above ground. Retro horror 2D game style. PNG format with transparent background, idle position facing forward.. In-Game asset. 2d. High contrast. No shadows
Create a pixel-art environment asset of the Galata Tower at night, in a dark and eerie style suitable for a 2D horror survival game. The tower should be tall and imposing with its recognizable cylindrical stone structure and conical roof. Add cracks, overgrown vines, and dim glowing windows to give it a haunted, ancient look. A full moon and cloudy night sky in the background. Mist or fog at the base. The overall atmosphere should feel ominous and mysterious. PNG format with transparent background.. In-Game asset. 2d. High contrast. No shadows
Create a pixel-art ghostly character based on the Turkish folklore concept of a "cin" (djinn). The character should have a semi-transparent, smoky body with ethereal, shifting edges. Its face should be menacing, with glowing green or blue eyes and a faint, wicked smile. Add small, broken chains or mystical symbols faintly visible on its body to suggest it was once sealed. It should be levitating, with ghost-like movement and a cursed aura. The color palette should include shades of dark blue, purple, and gray. 2D horror game style, PNG with transparent background, idle or float animation frame.. In-Game asset. 2d. High contrast. No shadows
Create a pixel-art item asset of an ancient talisman inspired by Turkish folklore. It should appear as a folded parchment wrapped in an old red thread, with faint Ottoman-style calligraphy or mystical symbols visible. The talisman should have a glowing aura or faint sparkles to imply magical power. Optional details: slightly burned edges, aged paper texture, and traces of dried wax seal. The style should be mysterious, enchanted, and suitable for a 2D horror game. PNG format with transparent background, top-down or item-inventory view.. In-Game asset. 2d. High contrast. No shadows
Create a pixel-art item of an old, mysterious letter for a horror-themed 2D game. The letter should be a yellowed, slightly torn piece of parchment with faded ink handwriting on it. Include smudges, old blood stains, or water damage for a haunting effect. The handwriting should look rushed or distressed. Fold marks or a wax seal (partially broken) can be added as details. It should look like something found in an abandoned house or crypt. PNG format with transparent background, suitable for inventory or collectible items.. In-Game asset. 2d. High contrast. No shadows
Create a pixel-art main character sprite of a scared blonde woman for a 2D horror survival game. She should have light blonde hair (shoulder length or tied in a ponytail), wide anxious eyes, and a pale complexion. Her outfit should be casual: a light-colored shirt or hoodie, jeans, and sneakers. The character should appear visibly nervous or frightened – slightly hunched posture, hands close to body, maybe holding a flashlight. Include subtle animation or idle pose showing fear. Style should match retro horror games. PNG with transparent background, front-facing idle stance.. In-Game asset. 2d. High contrast. No shadows