/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
var DarknessMask = Container.expand(function () {
var self = Container.call(this);
// Create a dark overlay that covers the entire screen
var darkness = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 2048,
height: 2732,
alpha: 0.85,
tint: 0x000000
});
// Scale to cover the entire screen
darkness.width = 2048;
darkness.height = 2732;
self.addChild(darkness);
// Create a visible circle around player (light effect)
self.lightRadius = 250; // Reduced visibility radius for more tension
self.adjustLightRadius = function (playerNoiseLevel) {
// Light shrinks when making noise (harder to see when noisy)
var targetRadius = 250 - playerNoiseLevel * 80;
// Smooth transition with tween
tween(self, {
lightRadius: targetRadius
}, {
duration: 500
});
};
self.updatePosition = function (playerX, playerY) {
self.x = 2048 / 2;
self.y = 2732 / 2;
// Create a mask using a circle asset at player's position
if (!self.mask) {
// Create a circle mask using a shape asset instead of Graphics
self.mask = self.addChild(LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: self.lightRadius * 2,
height: self.lightRadius * 2,
tint: 0xFFFFFF
}));
}
// Update mask position
self.mask.x = playerX - self.x;
self.mask.y = playerY - self.y;
// Ensure mask size matches the desired light radius
self.mask.width = self.lightRadius * 2;
self.mask.height = self.lightRadius * 2;
};
return self;
});
var Exit = Container.expand(function () {
var self = Container.call(this);
var exitSprite = self.attachAsset('exit', {
anchorX: 0.5,
anchorY: 0.5
});
// Initially exits are inactive
exitSprite.alpha = 0.3;
self.activate = function () {
exitSprite.alpha = 1;
// Make the exit slightly pulse to draw attention
function pulse() {
tween(exitSprite, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 700,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(exitSprite, {
scaleX: 1,
scaleY: 1
}, {
duration: 700,
easing: tween.easeInOut,
onFinish: pulse
});
}
});
}
pulse();
};
return self;
});
var Locker = Container.expand(function () {
var self = Container.call(this);
var lockerSprite = self.attachAsset('locker', {
anchorX: 0.5,
anchorY: 0.5
});
self.isOccupied = false;
return self;
});
var Monster = Container.expand(function () {
var self = Container.call(this);
var monsterSprite = self.attachAsset('monster', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 2;
self.detectionRadius = 400;
self.hasSight = false;
self.targetX = 0;
self.targetY = 0;
self.isChasing = false;
self.wanderTimer = 0;
self.wanderDirection = {
x: 0,
y: 0
};
self.setHasSight = function (value) {
self.hasSight = value;
if (value) {
tween(monsterSprite, {
tint: 0xFF00FF
}, {
duration: 1000
});
self.speed = 3;
// Show "HE IS NOT BLIND!!" message when monster gains sight
if (instructionText) {
instructionText.setText('HE IS NOT BLIND!!');
instructionText.style.fill = "#FF0000";
// Flash the screen to indicate danger
LK.effects.flashScreen(0xFF0000, 500);
}
}
};
self.startChasing = function (x, y) {
self.isChasing = true;
self.targetX = x;
self.targetY = y;
LK.getSound('monster_detect').play();
// Increase speed temporarily during initial chase
self.speed = self.speed * 1.5;
LK.setTimeout(function () {
if (self.isChasing) {
self.speed = self.speed / 1.5;
}
}, 3000);
// Monster growls and bangs chest
function bangChest() {
tween(monsterSprite, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFF3300
}, {
duration: 200,
onFinish: function onFinish() {
tween(monsterSprite, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
if (self.isChasing && LK.ticks % 2 === 0) {
bangChest();
}
}
});
}
});
}
bangChest();
};
self.stopChasing = function () {
self.isChasing = false;
};
self.wander = function () {
if (LK.ticks % 120 === 0 || self.wanderTimer <= 0) {
// Pick a random direction
var angle = Math.random() * Math.PI * 2;
self.wanderDirection = {
x: Math.cos(angle),
y: Math.sin(angle)
};
self.wanderTimer = 60 + Math.random() * 120;
// Sometimes patrol near a locker
if (Math.random() < 0.3 && lockers && lockers.length > 0) {
// Target a random locker
var randomLocker = lockers[Math.floor(Math.random() * lockers.length)];
self.targetX = randomLocker.x;
self.targetY = randomLocker.y;
// Move toward locker for a while
LK.setTimeout(function () {
// Reset after approaching
self.targetX = 0;
self.targetY = 0;
}, 3000);
}
}
// If targeting a specific location during wandering
if (self.targetX !== 0 && self.targetY !== 0) {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 150) {
// Move toward target at reduced speed
self.x += dx / dist * (self.speed * 0.3);
self.y += dy / dist * (self.speed * 0.3);
} else {
// Wander normally when close to target
self.x += self.wanderDirection.x * (self.speed * 0.5);
self.y += self.wanderDirection.y * (self.speed * 0.5);
}
} else {
// Normal wandering
self.x += self.wanderDirection.x * (self.speed * 0.5);
self.y += self.wanderDirection.y * (self.speed * 0.5);
}
// Boundary checks
if (self.x < 100) self.x = 100;
if (self.x > 2048 - 100) self.x = 2048 - 100;
if (self.y < 100) self.y = 100;
if (self.y > 2732 - 100) self.y = 2732 - 100;
self.wanderTimer--;
};
self.moveTowardsTarget = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Basic pathfinding - try to avoid obstacles
var obstacleAhead = false;
// Check if there's a direct obstacle in movement path
for (var i = 0; walls && i < walls.length; i++) {
// Check only walls directly in path
var wallDx = walls[i].x - self.x;
var wallDy = walls[i].y - self.y;
var wallDist = Math.sqrt(wallDx * wallDx + wallDy * wallDy);
// Check if wall is within immediate path and roughly in the same direction
if (wallDist < 100 && (wallDx * dx + wallDy * dy) / (wallDist * distance) > 0.7) {
obstacleAhead = true;
break;
}
}
if (obstacleAhead && Math.random() < 0.5) {
// Add slight random variation to movement to navigate around obstacles
var angle = Math.atan2(dy, dx) + (Math.random() * 0.8 - 0.4);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
} else {
// Direct movement toward target
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
// Occasionally increase speed in bursts when chasing
if (LK.ticks % 180 === 0 && Math.random() < 0.3) {
var burstSpeed = self.speed * 1.5;
var originalSpeed = self.speed;
self.speed = burstSpeed;
LK.setTimeout(function () {
self.speed = originalSpeed;
}, 500);
}
}
}
};
self.update = function () {
if (self.isChasing) {
self.moveTowardsTarget();
} else {
self.wander();
}
};
return self;
});
var Notebook = Container.expand(function () {
var self = Container.call(this);
var notebookSprite = self.attachAsset('notebook', {
anchorX: 0.5,
anchorY: 0.5
});
// Make the notebook slightly pulse to draw attention
function pulse() {
tween(notebookSprite, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(notebookSprite, {
scaleX: 1,
scaleY: 1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: pulse
});
}
});
}
pulse();
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerSprite = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 4;
self.isHidden = false;
self.noiseLevel = 0;
self.notebooksCollected = 0;
self.makeNoise = function (amount) {
self.noiseLevel = Math.min(1, self.noiseLevel + amount);
// Noise decreases over time
LK.setTimeout(function () {
self.noiseLevel = Math.max(0, self.noiseLevel - 0.1);
}, 1000);
};
self.hide = function () {
if (!self.isHidden) {
self.isHidden = true;
// Fade out effect
tween(playerSprite, {
alpha: 0.3,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 300
});
LK.getSound('hide').play();
// Heavy breathing effect when monster is nearby
if (monster && monster.isChasing) {
// Trembling effect while hiding from monster
var _tremble = function tremble() {
if (self.isHidden && monster.isChasing) {
var trembleAmt = 0.05;
playerSprite.x = Math.random() * trembleAmt * 2 - trembleAmt;
playerSprite.y = Math.random() * trembleAmt * 2 - trembleAmt;
LK.setTimeout(_tremble, 100);
} else {
playerSprite.x = 0;
playerSprite.y = 0;
}
};
_tremble();
}
}
};
self.unhide = function () {
if (self.isHidden) {
self.isHidden = false;
playerSprite.alpha = 1;
}
};
self.update = function () {
// Player visibility logic is handled in the main game update
if (self.isMoving && !self.isHidden) {
if (LK.ticks % 20 === 0) {
LK.getSound('footstep').play();
self.makeNoise(0.15);
// Add subtle visual feedback for footsteps
tween(playerSprite, {
scaleX: 0.95,
scaleY: 1.05,
y: 5
}, {
duration: 150,
onFinish: function onFinish() {
tween(playerSprite, {
scaleX: 1,
scaleY: 1,
y: 0
}, {
duration: 150
});
}
});
}
// Create footstep effect
if (LK.ticks % 30 === 0 && self.noiseLevel > 0.4) {
// Add visual ripple for footsteps when being noisy
var footstepEffect = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
alpha: 0.3,
width: 20,
height: 20,
tint: 0xFFFFFF
});
self.addChild(footstepEffect);
// Animate and remove
tween(footstepEffect, {
width: 60,
height: 60,
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
self.removeChild(footstepEffect);
}
});
}
}
};
return self;
});
var Wall = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x050505
});
/****
* Game Code
****/
// Game variables
var player;
var monster;
var notebooks = [];
var lockers = [];
var exits = [];
var walls = [];
var notebooksRequired = 7;
var lastTouchedLocker = null;
var gamePhase = "collecting"; // collecting, escaping
// UI elements
var notebookCounter;
var noiseIndicator;
var instructionText;
// Create the UI
function setupUI() {
// Notebook counter
notebookCounter = new Text2('0/' + notebooksRequired, {
size: 80,
fill: 0xFFD700
});
notebookCounter.anchor.set(1, 0);
LK.gui.topRight.addChild(notebookCounter);
// Noise indicator
noiseIndicator = new Text2('Noise: ●', {
size: 60,
fill: 0x00FF00
});
noiseIndicator.anchor.set(0, 0);
LK.gui.topRight.addChild(noiseIndicator);
noiseIndicator.y = 100;
// Instruction text
instructionText = new Text2('Find and collect 7 notebooks to escape!', {
size: 50,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 0);
LK.gui.top.addChild(instructionText);
instructionText.y = 20;
}
// Generate a random position that isn't too close to existing objects
function getRandomPosition(minDistance) {
var x, y;
var validPosition = false;
while (!validPosition) {
x = 150 + Math.random() * (2048 - 300);
y = 150 + Math.random() * (2732 - 300);
validPosition = true;
// Check distance from player
if (player) {
var dxPlayer = x - player.x;
var dyPlayer = y - player.y;
var distPlayer = Math.sqrt(dxPlayer * dxPlayer + dyPlayer * dyPlayer);
if (distPlayer < minDistance) {
validPosition = false;
continue;
}
}
// Check distance from other notebooks
for (var i = 0; i < notebooks.length; i++) {
var dxNote = x - notebooks[i].x;
var dyNote = y - notebooks[i].y;
var distNote = Math.sqrt(dxNote * dxNote + dyNote * dyNote);
if (distNote < minDistance) {
validPosition = false;
break;
}
}
// Check distance from lockers
for (var j = 0; j < lockers.length; j++) {
var dxLocker = x - lockers[j].x;
var dyLocker = y - lockers[j].y;
var distLocker = Math.sqrt(dxLocker * dxLocker + dyLocker * dyLocker);
if (distLocker < minDistance) {
validPosition = false;
break;
}
}
}
return {
x: x,
y: y
};
}
// Generate the game level
function setupLevel() {
// Create player
player = new Player();
player.x = 2048 / 2;
player.y = 2732 / 2;
game.addChild(player);
// Add darkness overlay with light around player
var darknessMask = new DarknessMask();
darknessMask.updatePosition(player.x, player.y);
game.addChild(darknessMask);
// Create school environment with walls
function createSchoolWalls() {
// Create outer walls (school boundaries)
// Top wall
for (var x = 0; x < 2048; x += 50) {
var topWall = new Wall();
topWall.x = x;
topWall.y = 25;
walls.push(topWall);
game.addChild(topWall);
}
// Bottom wall
for (var x = 0; x < 2048; x += 50) {
var bottomWall = new Wall();
bottomWall.x = x;
bottomWall.y = 2732 - 25;
walls.push(bottomWall);
game.addChild(bottomWall);
}
// Left wall
for (var y = 0; y < 2732; y += 50) {
var leftWall = new Wall();
leftWall.x = 25;
leftWall.y = y;
walls.push(leftWall);
game.addChild(leftWall);
}
// Right wall
for (var y = 0; y < 2732; y += 50) {
var rightWall = new Wall();
rightWall.x = 2048 - 25;
rightWall.y = y;
walls.push(rightWall);
game.addChild(rightWall);
}
// Create internal school hallways and rooms
// Create random corridors
var corridorCount = 8;
for (var i = 0; i < corridorCount; i++) {
var isHorizontal = Math.random() > 0.5;
var startX = 100 + Math.random() * (2048 - 200);
var startY = 100 + Math.random() * (2732 - 200);
var length = 300 + Math.random() * 500;
if (isHorizontal) {
for (var x = 0; x < length; x += 100) {
if (startX + x < 2048 - 100) {
// Add walls on sides of corridor
var topWall = new Wall();
topWall.x = startX + x;
topWall.y = startY - 50;
walls.push(topWall);
game.addChild(topWall);
var bottomWall = new Wall();
bottomWall.x = startX + x;
bottomWall.y = startY + 50;
walls.push(bottomWall);
game.addChild(bottomWall);
}
}
} else {
for (var y = 0; y < length; y += 100) {
if (startY + y < 2732 - 100) {
// Add walls on sides of corridor
var leftWall = new Wall();
leftWall.x = startX - 50;
leftWall.y = startY + y;
walls.push(leftWall);
game.addChild(leftWall);
var rightWall = new Wall();
rightWall.x = startX + 50;
rightWall.y = startY + y;
walls.push(rightWall);
game.addChild(rightWall);
}
}
}
}
}
createSchoolWalls();
// Create monster
monster = new Monster();
monster.x = 300;
monster.y = 300;
game.addChild(monster);
// Create notebooks
for (var i = 0; i < notebooksRequired; i++) {
var pos = getRandomPosition(300);
var notebook = new Notebook();
notebook.x = pos.x;
notebook.y = pos.y;
notebooks.push(notebook);
game.addChild(notebook);
}
// Create lockers
var lockerCount = 12;
for (var j = 0; j < lockerCount; j++) {
var lockerPos = getRandomPosition(250);
var locker = new Locker();
locker.x = lockerPos.x;
locker.y = lockerPos.y;
lockers.push(locker);
game.addChild(locker);
}
// Create exits (initially inactive)
var exitPositions = [{
x: 100,
y: 100
}, {
x: 2048 - 100,
y: 100
}, {
x: 2048 / 2,
y: 100
}, {
x: 100,
y: 2732 - 100
}, {
x: 2048 - 100,
y: 2732 - 100
}, {
x: 2048 / 2,
y: 2732 - 100
}];
for (var k = 0; k < exitPositions.length; k++) {
var exit = new Exit();
exit.x = exitPositions[k].x;
exit.y = exitPositions[k].y;
exits.push(exit);
game.addChild(exit);
}
}
// Start the game
function startGame() {
setupUI();
setupLevel();
// Play ambient music
LK.playMusic('ambient');
// Show tutorial message
var tutorialText = new Text2('Drag to move. Find notebooks.\nFlumbo Jumbo is blind but can hear you.\nMove carefully to stay quiet.\nHide in lockers when detected!', {
size: 40,
fill: 0xFFFFFF
});
tutorialText.anchor.set(0.5, 0.5);
tutorialText.x = 2048 / 2;
tutorialText.y = 2732 / 2;
game.addChild(tutorialText);
// Fade out tutorial after a delay
LK.setTimeout(function () {
tween(tutorialText, {
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
game.removeChild(tutorialText);
}
});
}, 5000);
}
// Input handling
var isDragging = false;
var lastTouchX = 0;
var lastTouchY = 0;
game.down = function (x, y, obj) {
isDragging = true;
lastTouchX = x;
lastTouchY = y;
};
game.up = function (x, y, obj) {
isDragging = false;
player.isMoving = false;
};
game.move = function (x, y, obj) {
if (isDragging && !player.isHidden) {
// Calculate direction and distance
var dx = x - lastTouchX;
var dy = y - lastTouchY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only move if there's significant movement
if (distance > 5) {
player.isMoving = true;
// Calculate speed based on noise level - more careful = less noise
var speedMultiplier = distance > 30 ? 0.6 : 0.4;
player.x += dx * speedMultiplier;
player.y += dy * speedMultiplier;
// If moving fast, generate more noise
if (distance > 30) {
player.makeNoise(0.01);
}
// Keep player in bounds
if (player.x < 50) player.x = 50;
if (player.x > 2048 - 50) player.x = 2048 - 50;
if (player.y < 50) player.y = 50;
if (player.y > 2732 - 50) player.y = 2732 - 50;
// Update last position
lastTouchX = x;
lastTouchY = y;
} else {
player.isMoving = false;
}
}
};
// Update game state
game.update = function () {
if (!player) {
startGame();
return;
}
// Update noise indicator
if (player.noiseLevel < 0.3) {
noiseIndicator.setText('Noise: ●');
noiseIndicator.style.fill = "#00FF00";
} else if (player.noiseLevel < 0.7) {
noiseIndicator.setText('Noise: ●●');
noiseIndicator.style.fill = "#FFFF00";
} else {
noiseIndicator.setText('Noise: ●●●');
noiseIndicator.style.fill = "#FF0000";
}
// Check for notebook collection
for (var i = notebooks.length - 1; i >= 0; i--) {
var notebook = notebooks[i];
if (player.intersects(notebook) && !player.isHidden) {
// Remove the notebook
notebook.destroy();
notebooks.splice(i, 1);
// Update player stats
player.notebooksCollected++;
notebookCounter.setText(player.notebooksCollected + '/' + notebooksRequired);
// Play sound
LK.getSound('pickup').play();
// Make some noise when collecting
player.makeNoise(0.3);
// Visual effect for notebook collection
var collectEffect = LK.getAsset('notebook', {
anchorX: 0.5,
anchorY: 0.5,
x: notebook.x,
y: notebook.y,
alpha: 0.8,
scaleX: 1,
scaleY: 1
});
game.addChild(collectEffect);
// Animation for notebook collection
tween(collectEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0,
y: collectEffect.y - 100
}, {
duration: 800,
onFinish: function onFinish() {
game.removeChild(collectEffect);
}
});
// Check if all notebooks are collected
if (player.notebooksCollected >= notebooksRequired) {
gamePhase = "escaping";
instructionText.setText('All notebooks collected! Find an exit before Flumbo catches you!');
monster.setHasSight(true);
// Dramatic effect when monster gains sight
LK.effects.flashScreen(0xFF0000, 800);
// Only activate the last exit (6th one) as instructed
// The other exits should be visible but closed
for (var e = 0; e < exits.length; e++) {
if (e === exits.length - 1) {
// Activate only the last exit
exits[e].activate();
// Make a subtle pulsing effect to hint at correct exit
LK.setTimeout(function () {
tween(exits[exits.length - 1].children[0], {
alpha: 0.7
}, {
duration: 500,
onFinish: function onFinish() {
tween(exits[exits.length - 1].children[0], {
alpha: 1
}, {
duration: 500,
onFinish: function onFinish() {
if (gamePhase === "escaping") {
this.onFinish();
}
}
});
}
});
}, 1000);
} else {
// Make other exits visible but not usable
var exitSprite = exits[e].children[0];
exitSprite.alpha = 0.5;
exitSprite.tint = 0x880000; // Red tint to indicate closed
}
}
}
}
}
// Check for hiding in lockers
var nearLocker = false;
for (var j = 0; j < lockers.length; j++) {
var locker = lockers[j];
var dxLocker = player.x - locker.x;
var dyLocker = player.y - locker.y;
var distLocker = Math.sqrt(dxLocker * dxLocker + dyLocker * dyLocker);
if (distLocker < 100 && !locker.isOccupied) {
nearLocker = true;
lastTouchedLocker = locker;
// Double tap or tap when monster is chasing to hide
if (isDragging && (monster.isChasing || LK.ticks % 30 === 0)) {
player.hide();
player.x = locker.x;
player.y = locker.y;
locker.isOccupied = true;
break;
}
}
}
// Allow player to exit locker when not being chased
if (player.isHidden && !monster.isChasing && isDragging) {
player.unhide();
if (lastTouchedLocker) {
lastTouchedLocker.isOccupied = false;
}
}
// Check for reaching an exit in escape phase
if (gamePhase === "escaping") {
for (var k = 0; k < exits.length; k++) {
var exit = exits[k];
if (player.intersects(exit) && !player.isHidden) {
// Check if this is the last exit (the only open one)
if (k === exits.length - 1) {
// Play escape cutscene
player.isHidden = true; // Prevent further interaction
monster.isChasing = true;
monster.targetX = player.x;
monster.targetY = player.y;
// Player slides through the exit
tween(player, {
x: exit.x,
y: exit.y,
alpha: 0.5,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
onFinish: function onFinish() {
// Monster gets crushed by the door - dramatic chase sequence
tween(monster, {
x: exit.x - 50,
y: exit.y
}, {
duration: 600,
onFinish: function onFinish() {
// Monster tries to reach player in last desperate attempt
tween(monster.children[0], {
scaleX: 1.3,
scaleY: 1.1,
tint: 0xFF0000
}, {
duration: 300,
onFinish: function onFinish() {
// Door closes on monster
LK.effects.flashScreen(0xFF0000, 500);
// Play monster detection sound for impact
LK.getSound('monster_detect').play();
// Flatten monster to show crushing
tween(monster.children[0], {
scaleY: 0.2,
scaleX: 1.8,
alpha: 0.5,
tint: 0x660000
}, {
duration: 700,
onFinish: function onFinish() {
// Show "soft place center" message
instructionText.setText("You've escaped to the soft place center! Chapter complete.");
instructionText.style.fill = "#00FF00";
// Create final escape visual effect
var light = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: exit.x,
y: exit.y,
alpha: 0,
width: 50,
height: 50,
tint: 0xFFFFFF
});
game.addChild(light);
tween(light, {
alpha: 0.8,
width: 2048,
height: 2048
}, {
duration: 1500,
onFinish: function onFinish() {
LK.setTimeout(function () {
LK.showYouWin();
}, 500);
}
});
}
});
}
});
}
});
}
});
return;
} else {
// Closed exit - monster gets angrier
LK.effects.flashScreen(0xFF0000, 300);
monster.speed += 0.5;
monster.startChasing(player.x, player.y);
}
}
}
}
// Monster detection
var dxMonster = monster.x - player.x;
var dyMonster = monster.y - player.y;
var distMonster = Math.sqrt(dxMonster * dxMonster + dyMonster * dyMonster);
// Proximity warning - monster is getting close
if (!player.isHidden && distMonster < monster.detectionRadius * 1.5 && !monster.isChasing) {
// Visual proximity indicator
if (LK.ticks % 60 === 0) {
LK.effects.flashScreen(0x330000, 300);
// Increase player fear based on proximity
var proximityFear = 1 - distMonster / (monster.detectionRadius * 1.5);
player.makeNoise(proximityFear * 0.1); // Fear increases noise level slightly
// Heartbeat effect - the closer the monster, the faster the heartbeat
var heartbeatIntensity = 1 - distMonster / (monster.detectionRadius * 1.5);
// Pulse the player slightly to simulate heartbeat
tween(player, {
scaleX: 1 + heartbeatIntensity * 0.1,
scaleY: 1 + heartbeatIntensity * 0.1
}, {
duration: 300 - heartbeatIntensity * 200,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 300 - heartbeatIntensity * 200
});
}
});
}
}
// Monster detects player based on noise
if (!player.isHidden && distMonster < monster.detectionRadius * player.noiseLevel) {
monster.startChasing(player.x, player.y);
// Show warning message as instructed
instructionText.setText('RUN AND GO FIND A LOCKER!!');
instructionText.style.fill = "#FF0000";
}
// Monster can see player in the final phase
else if (gamePhase === "escaping" && monster.hasSight && !player.isHidden && distMonster < monster.detectionRadius * 1.5) {
monster.startChasing(player.x, player.y);
}
// Update monster's target position if already chasing
else if (monster.isChasing && !player.isHidden) {
monster.targetX = player.x;
monster.targetY = player.y;
}
// Stop chasing if player is hidden
else if (player.isHidden && monster.isChasing) {
LK.setTimeout(function () {
monster.stopChasing();
}, 1500);
}
// Stop chasing if lost track for too long
else if (monster.isChasing && LK.ticks % 180 === 0) {
monster.stopChasing();
}
// Check for collisions with walls
function checkWallCollisions() {
// Check player-wall collisions
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (player.intersects(wall) && !player.isHidden) {
// Calculate push back direction
var dx = player.x - wall.x;
var dy = player.y - wall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// Push player away from wall
if (dist > 0) {
// Calculate minimal push needed
var overlapX = (player.width + wall.width) / 2 - Math.abs(dx);
var overlapY = (player.height + wall.height) / 2 - Math.abs(dy);
// Push in the direction of minimum overlap
if (overlapX < overlapY) {
player.x += dx / dist * overlapX * 1.1;
} else {
player.y += dy / dist * overlapY * 1.1;
}
}
}
}
// Check monster-wall collisions (less strict, monster can navigate around walls better)
if (monster.isChasing) {
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (monster.intersects(wall)) {
// Less strict collision for monster - allows some navigation through gaps
var dx = monster.x - wall.x;
var dy = monster.y - wall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
monster.x += dx / dist * 2;
monster.y += dy / dist * 2;
}
}
}
}
}
checkWallCollisions();
// Update darkness mask to follow player
for (var i = 0; i < game.children.length; i++) {
if (game.children[i] instanceof DarknessMask) {
game.children[i].updatePosition(player.x, player.y);
// Also adjust light radius based on player noise
game.children[i].adjustLightRadius(player.noiseLevel);
break;
}
}
// Check for player caught by monster
if (!player.isHidden && player.intersects(monster)) {
// Flash the screen red
LK.effects.flashScreen(0xFF0000, 500);
// Game over
LK.showGameOver();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
var facekit = LK.import("@upit/facekit.v1");
/****
* Classes
****/
var DarknessMask = Container.expand(function () {
var self = Container.call(this);
// Create a dark overlay that covers the entire screen
var darkness = LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: 2048,
height: 2732,
alpha: 0.85,
tint: 0x000000
});
// Scale to cover the entire screen
darkness.width = 2048;
darkness.height = 2732;
self.addChild(darkness);
// Create a visible circle around player (light effect)
self.lightRadius = 250; // Reduced visibility radius for more tension
self.adjustLightRadius = function (playerNoiseLevel) {
// Light shrinks when making noise (harder to see when noisy)
var targetRadius = 250 - playerNoiseLevel * 80;
// Smooth transition with tween
tween(self, {
lightRadius: targetRadius
}, {
duration: 500
});
};
self.updatePosition = function (playerX, playerY) {
self.x = 2048 / 2;
self.y = 2732 / 2;
// Create a mask using a circle asset at player's position
if (!self.mask) {
// Create a circle mask using a shape asset instead of Graphics
self.mask = self.addChild(LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: self.lightRadius * 2,
height: self.lightRadius * 2,
tint: 0xFFFFFF
}));
}
// Update mask position
self.mask.x = playerX - self.x;
self.mask.y = playerY - self.y;
// Ensure mask size matches the desired light radius
self.mask.width = self.lightRadius * 2;
self.mask.height = self.lightRadius * 2;
};
return self;
});
var Exit = Container.expand(function () {
var self = Container.call(this);
var exitSprite = self.attachAsset('exit', {
anchorX: 0.5,
anchorY: 0.5
});
// Initially exits are inactive
exitSprite.alpha = 0.3;
self.activate = function () {
exitSprite.alpha = 1;
// Make the exit slightly pulse to draw attention
function pulse() {
tween(exitSprite, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 700,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(exitSprite, {
scaleX: 1,
scaleY: 1
}, {
duration: 700,
easing: tween.easeInOut,
onFinish: pulse
});
}
});
}
pulse();
};
return self;
});
var Locker = Container.expand(function () {
var self = Container.call(this);
var lockerSprite = self.attachAsset('locker', {
anchorX: 0.5,
anchorY: 0.5
});
self.isOccupied = false;
return self;
});
var Monster = Container.expand(function () {
var self = Container.call(this);
var monsterSprite = self.attachAsset('monster', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 2;
self.detectionRadius = 400;
self.hasSight = false;
self.targetX = 0;
self.targetY = 0;
self.isChasing = false;
self.wanderTimer = 0;
self.wanderDirection = {
x: 0,
y: 0
};
self.setHasSight = function (value) {
self.hasSight = value;
if (value) {
tween(monsterSprite, {
tint: 0xFF00FF
}, {
duration: 1000
});
self.speed = 3;
// Show "HE IS NOT BLIND!!" message when monster gains sight
if (instructionText) {
instructionText.setText('HE IS NOT BLIND!!');
instructionText.style.fill = "#FF0000";
// Flash the screen to indicate danger
LK.effects.flashScreen(0xFF0000, 500);
}
}
};
self.startChasing = function (x, y) {
self.isChasing = true;
self.targetX = x;
self.targetY = y;
LK.getSound('monster_detect').play();
// Increase speed temporarily during initial chase
self.speed = self.speed * 1.5;
LK.setTimeout(function () {
if (self.isChasing) {
self.speed = self.speed / 1.5;
}
}, 3000);
// Monster growls and bangs chest
function bangChest() {
tween(monsterSprite, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFF3300
}, {
duration: 200,
onFinish: function onFinish() {
tween(monsterSprite, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
if (self.isChasing && LK.ticks % 2 === 0) {
bangChest();
}
}
});
}
});
}
bangChest();
};
self.stopChasing = function () {
self.isChasing = false;
};
self.wander = function () {
if (LK.ticks % 120 === 0 || self.wanderTimer <= 0) {
// Pick a random direction
var angle = Math.random() * Math.PI * 2;
self.wanderDirection = {
x: Math.cos(angle),
y: Math.sin(angle)
};
self.wanderTimer = 60 + Math.random() * 120;
// Sometimes patrol near a locker
if (Math.random() < 0.3 && lockers && lockers.length > 0) {
// Target a random locker
var randomLocker = lockers[Math.floor(Math.random() * lockers.length)];
self.targetX = randomLocker.x;
self.targetY = randomLocker.y;
// Move toward locker for a while
LK.setTimeout(function () {
// Reset after approaching
self.targetX = 0;
self.targetY = 0;
}, 3000);
}
}
// If targeting a specific location during wandering
if (self.targetX !== 0 && self.targetY !== 0) {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 150) {
// Move toward target at reduced speed
self.x += dx / dist * (self.speed * 0.3);
self.y += dy / dist * (self.speed * 0.3);
} else {
// Wander normally when close to target
self.x += self.wanderDirection.x * (self.speed * 0.5);
self.y += self.wanderDirection.y * (self.speed * 0.5);
}
} else {
// Normal wandering
self.x += self.wanderDirection.x * (self.speed * 0.5);
self.y += self.wanderDirection.y * (self.speed * 0.5);
}
// Boundary checks
if (self.x < 100) self.x = 100;
if (self.x > 2048 - 100) self.x = 2048 - 100;
if (self.y < 100) self.y = 100;
if (self.y > 2732 - 100) self.y = 2732 - 100;
self.wanderTimer--;
};
self.moveTowardsTarget = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Basic pathfinding - try to avoid obstacles
var obstacleAhead = false;
// Check if there's a direct obstacle in movement path
for (var i = 0; walls && i < walls.length; i++) {
// Check only walls directly in path
var wallDx = walls[i].x - self.x;
var wallDy = walls[i].y - self.y;
var wallDist = Math.sqrt(wallDx * wallDx + wallDy * wallDy);
// Check if wall is within immediate path and roughly in the same direction
if (wallDist < 100 && (wallDx * dx + wallDy * dy) / (wallDist * distance) > 0.7) {
obstacleAhead = true;
break;
}
}
if (obstacleAhead && Math.random() < 0.5) {
// Add slight random variation to movement to navigate around obstacles
var angle = Math.atan2(dy, dx) + (Math.random() * 0.8 - 0.4);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
} else {
// Direct movement toward target
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
// Occasionally increase speed in bursts when chasing
if (LK.ticks % 180 === 0 && Math.random() < 0.3) {
var burstSpeed = self.speed * 1.5;
var originalSpeed = self.speed;
self.speed = burstSpeed;
LK.setTimeout(function () {
self.speed = originalSpeed;
}, 500);
}
}
}
};
self.update = function () {
if (self.isChasing) {
self.moveTowardsTarget();
} else {
self.wander();
}
};
return self;
});
var Notebook = Container.expand(function () {
var self = Container.call(this);
var notebookSprite = self.attachAsset('notebook', {
anchorX: 0.5,
anchorY: 0.5
});
// Make the notebook slightly pulse to draw attention
function pulse() {
tween(notebookSprite, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(notebookSprite, {
scaleX: 1,
scaleY: 1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: pulse
});
}
});
}
pulse();
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerSprite = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 4;
self.isHidden = false;
self.noiseLevel = 0;
self.notebooksCollected = 0;
self.makeNoise = function (amount) {
self.noiseLevel = Math.min(1, self.noiseLevel + amount);
// Noise decreases over time
LK.setTimeout(function () {
self.noiseLevel = Math.max(0, self.noiseLevel - 0.1);
}, 1000);
};
self.hide = function () {
if (!self.isHidden) {
self.isHidden = true;
// Fade out effect
tween(playerSprite, {
alpha: 0.3,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 300
});
LK.getSound('hide').play();
// Heavy breathing effect when monster is nearby
if (monster && monster.isChasing) {
// Trembling effect while hiding from monster
var _tremble = function tremble() {
if (self.isHidden && monster.isChasing) {
var trembleAmt = 0.05;
playerSprite.x = Math.random() * trembleAmt * 2 - trembleAmt;
playerSprite.y = Math.random() * trembleAmt * 2 - trembleAmt;
LK.setTimeout(_tremble, 100);
} else {
playerSprite.x = 0;
playerSprite.y = 0;
}
};
_tremble();
}
}
};
self.unhide = function () {
if (self.isHidden) {
self.isHidden = false;
playerSprite.alpha = 1;
}
};
self.update = function () {
// Player visibility logic is handled in the main game update
if (self.isMoving && !self.isHidden) {
if (LK.ticks % 20 === 0) {
LK.getSound('footstep').play();
self.makeNoise(0.15);
// Add subtle visual feedback for footsteps
tween(playerSprite, {
scaleX: 0.95,
scaleY: 1.05,
y: 5
}, {
duration: 150,
onFinish: function onFinish() {
tween(playerSprite, {
scaleX: 1,
scaleY: 1,
y: 0
}, {
duration: 150
});
}
});
}
// Create footstep effect
if (LK.ticks % 30 === 0 && self.noiseLevel > 0.4) {
// Add visual ripple for footsteps when being noisy
var footstepEffect = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
alpha: 0.3,
width: 20,
height: 20,
tint: 0xFFFFFF
});
self.addChild(footstepEffect);
// Animate and remove
tween(footstepEffect, {
width: 60,
height: 60,
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
self.removeChild(footstepEffect);
}
});
}
}
};
return self;
});
var Wall = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x050505
});
/****
* Game Code
****/
// Game variables
var player;
var monster;
var notebooks = [];
var lockers = [];
var exits = [];
var walls = [];
var notebooksRequired = 7;
var lastTouchedLocker = null;
var gamePhase = "collecting"; // collecting, escaping
// UI elements
var notebookCounter;
var noiseIndicator;
var instructionText;
// Create the UI
function setupUI() {
// Notebook counter
notebookCounter = new Text2('0/' + notebooksRequired, {
size: 80,
fill: 0xFFD700
});
notebookCounter.anchor.set(1, 0);
LK.gui.topRight.addChild(notebookCounter);
// Noise indicator
noiseIndicator = new Text2('Noise: ●', {
size: 60,
fill: 0x00FF00
});
noiseIndicator.anchor.set(0, 0);
LK.gui.topRight.addChild(noiseIndicator);
noiseIndicator.y = 100;
// Instruction text
instructionText = new Text2('Find and collect 7 notebooks to escape!', {
size: 50,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 0);
LK.gui.top.addChild(instructionText);
instructionText.y = 20;
}
// Generate a random position that isn't too close to existing objects
function getRandomPosition(minDistance) {
var x, y;
var validPosition = false;
while (!validPosition) {
x = 150 + Math.random() * (2048 - 300);
y = 150 + Math.random() * (2732 - 300);
validPosition = true;
// Check distance from player
if (player) {
var dxPlayer = x - player.x;
var dyPlayer = y - player.y;
var distPlayer = Math.sqrt(dxPlayer * dxPlayer + dyPlayer * dyPlayer);
if (distPlayer < minDistance) {
validPosition = false;
continue;
}
}
// Check distance from other notebooks
for (var i = 0; i < notebooks.length; i++) {
var dxNote = x - notebooks[i].x;
var dyNote = y - notebooks[i].y;
var distNote = Math.sqrt(dxNote * dxNote + dyNote * dyNote);
if (distNote < minDistance) {
validPosition = false;
break;
}
}
// Check distance from lockers
for (var j = 0; j < lockers.length; j++) {
var dxLocker = x - lockers[j].x;
var dyLocker = y - lockers[j].y;
var distLocker = Math.sqrt(dxLocker * dxLocker + dyLocker * dyLocker);
if (distLocker < minDistance) {
validPosition = false;
break;
}
}
}
return {
x: x,
y: y
};
}
// Generate the game level
function setupLevel() {
// Create player
player = new Player();
player.x = 2048 / 2;
player.y = 2732 / 2;
game.addChild(player);
// Add darkness overlay with light around player
var darknessMask = new DarknessMask();
darknessMask.updatePosition(player.x, player.y);
game.addChild(darknessMask);
// Create school environment with walls
function createSchoolWalls() {
// Create outer walls (school boundaries)
// Top wall
for (var x = 0; x < 2048; x += 50) {
var topWall = new Wall();
topWall.x = x;
topWall.y = 25;
walls.push(topWall);
game.addChild(topWall);
}
// Bottom wall
for (var x = 0; x < 2048; x += 50) {
var bottomWall = new Wall();
bottomWall.x = x;
bottomWall.y = 2732 - 25;
walls.push(bottomWall);
game.addChild(bottomWall);
}
// Left wall
for (var y = 0; y < 2732; y += 50) {
var leftWall = new Wall();
leftWall.x = 25;
leftWall.y = y;
walls.push(leftWall);
game.addChild(leftWall);
}
// Right wall
for (var y = 0; y < 2732; y += 50) {
var rightWall = new Wall();
rightWall.x = 2048 - 25;
rightWall.y = y;
walls.push(rightWall);
game.addChild(rightWall);
}
// Create internal school hallways and rooms
// Create random corridors
var corridorCount = 8;
for (var i = 0; i < corridorCount; i++) {
var isHorizontal = Math.random() > 0.5;
var startX = 100 + Math.random() * (2048 - 200);
var startY = 100 + Math.random() * (2732 - 200);
var length = 300 + Math.random() * 500;
if (isHorizontal) {
for (var x = 0; x < length; x += 100) {
if (startX + x < 2048 - 100) {
// Add walls on sides of corridor
var topWall = new Wall();
topWall.x = startX + x;
topWall.y = startY - 50;
walls.push(topWall);
game.addChild(topWall);
var bottomWall = new Wall();
bottomWall.x = startX + x;
bottomWall.y = startY + 50;
walls.push(bottomWall);
game.addChild(bottomWall);
}
}
} else {
for (var y = 0; y < length; y += 100) {
if (startY + y < 2732 - 100) {
// Add walls on sides of corridor
var leftWall = new Wall();
leftWall.x = startX - 50;
leftWall.y = startY + y;
walls.push(leftWall);
game.addChild(leftWall);
var rightWall = new Wall();
rightWall.x = startX + 50;
rightWall.y = startY + y;
walls.push(rightWall);
game.addChild(rightWall);
}
}
}
}
}
createSchoolWalls();
// Create monster
monster = new Monster();
monster.x = 300;
monster.y = 300;
game.addChild(monster);
// Create notebooks
for (var i = 0; i < notebooksRequired; i++) {
var pos = getRandomPosition(300);
var notebook = new Notebook();
notebook.x = pos.x;
notebook.y = pos.y;
notebooks.push(notebook);
game.addChild(notebook);
}
// Create lockers
var lockerCount = 12;
for (var j = 0; j < lockerCount; j++) {
var lockerPos = getRandomPosition(250);
var locker = new Locker();
locker.x = lockerPos.x;
locker.y = lockerPos.y;
lockers.push(locker);
game.addChild(locker);
}
// Create exits (initially inactive)
var exitPositions = [{
x: 100,
y: 100
}, {
x: 2048 - 100,
y: 100
}, {
x: 2048 / 2,
y: 100
}, {
x: 100,
y: 2732 - 100
}, {
x: 2048 - 100,
y: 2732 - 100
}, {
x: 2048 / 2,
y: 2732 - 100
}];
for (var k = 0; k < exitPositions.length; k++) {
var exit = new Exit();
exit.x = exitPositions[k].x;
exit.y = exitPositions[k].y;
exits.push(exit);
game.addChild(exit);
}
}
// Start the game
function startGame() {
setupUI();
setupLevel();
// Play ambient music
LK.playMusic('ambient');
// Show tutorial message
var tutorialText = new Text2('Drag to move. Find notebooks.\nFlumbo Jumbo is blind but can hear you.\nMove carefully to stay quiet.\nHide in lockers when detected!', {
size: 40,
fill: 0xFFFFFF
});
tutorialText.anchor.set(0.5, 0.5);
tutorialText.x = 2048 / 2;
tutorialText.y = 2732 / 2;
game.addChild(tutorialText);
// Fade out tutorial after a delay
LK.setTimeout(function () {
tween(tutorialText, {
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
game.removeChild(tutorialText);
}
});
}, 5000);
}
// Input handling
var isDragging = false;
var lastTouchX = 0;
var lastTouchY = 0;
game.down = function (x, y, obj) {
isDragging = true;
lastTouchX = x;
lastTouchY = y;
};
game.up = function (x, y, obj) {
isDragging = false;
player.isMoving = false;
};
game.move = function (x, y, obj) {
if (isDragging && !player.isHidden) {
// Calculate direction and distance
var dx = x - lastTouchX;
var dy = y - lastTouchY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only move if there's significant movement
if (distance > 5) {
player.isMoving = true;
// Calculate speed based on noise level - more careful = less noise
var speedMultiplier = distance > 30 ? 0.6 : 0.4;
player.x += dx * speedMultiplier;
player.y += dy * speedMultiplier;
// If moving fast, generate more noise
if (distance > 30) {
player.makeNoise(0.01);
}
// Keep player in bounds
if (player.x < 50) player.x = 50;
if (player.x > 2048 - 50) player.x = 2048 - 50;
if (player.y < 50) player.y = 50;
if (player.y > 2732 - 50) player.y = 2732 - 50;
// Update last position
lastTouchX = x;
lastTouchY = y;
} else {
player.isMoving = false;
}
}
};
// Update game state
game.update = function () {
if (!player) {
startGame();
return;
}
// Update noise indicator
if (player.noiseLevel < 0.3) {
noiseIndicator.setText('Noise: ●');
noiseIndicator.style.fill = "#00FF00";
} else if (player.noiseLevel < 0.7) {
noiseIndicator.setText('Noise: ●●');
noiseIndicator.style.fill = "#FFFF00";
} else {
noiseIndicator.setText('Noise: ●●●');
noiseIndicator.style.fill = "#FF0000";
}
// Check for notebook collection
for (var i = notebooks.length - 1; i >= 0; i--) {
var notebook = notebooks[i];
if (player.intersects(notebook) && !player.isHidden) {
// Remove the notebook
notebook.destroy();
notebooks.splice(i, 1);
// Update player stats
player.notebooksCollected++;
notebookCounter.setText(player.notebooksCollected + '/' + notebooksRequired);
// Play sound
LK.getSound('pickup').play();
// Make some noise when collecting
player.makeNoise(0.3);
// Visual effect for notebook collection
var collectEffect = LK.getAsset('notebook', {
anchorX: 0.5,
anchorY: 0.5,
x: notebook.x,
y: notebook.y,
alpha: 0.8,
scaleX: 1,
scaleY: 1
});
game.addChild(collectEffect);
// Animation for notebook collection
tween(collectEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0,
y: collectEffect.y - 100
}, {
duration: 800,
onFinish: function onFinish() {
game.removeChild(collectEffect);
}
});
// Check if all notebooks are collected
if (player.notebooksCollected >= notebooksRequired) {
gamePhase = "escaping";
instructionText.setText('All notebooks collected! Find an exit before Flumbo catches you!');
monster.setHasSight(true);
// Dramatic effect when monster gains sight
LK.effects.flashScreen(0xFF0000, 800);
// Only activate the last exit (6th one) as instructed
// The other exits should be visible but closed
for (var e = 0; e < exits.length; e++) {
if (e === exits.length - 1) {
// Activate only the last exit
exits[e].activate();
// Make a subtle pulsing effect to hint at correct exit
LK.setTimeout(function () {
tween(exits[exits.length - 1].children[0], {
alpha: 0.7
}, {
duration: 500,
onFinish: function onFinish() {
tween(exits[exits.length - 1].children[0], {
alpha: 1
}, {
duration: 500,
onFinish: function onFinish() {
if (gamePhase === "escaping") {
this.onFinish();
}
}
});
}
});
}, 1000);
} else {
// Make other exits visible but not usable
var exitSprite = exits[e].children[0];
exitSprite.alpha = 0.5;
exitSprite.tint = 0x880000; // Red tint to indicate closed
}
}
}
}
}
// Check for hiding in lockers
var nearLocker = false;
for (var j = 0; j < lockers.length; j++) {
var locker = lockers[j];
var dxLocker = player.x - locker.x;
var dyLocker = player.y - locker.y;
var distLocker = Math.sqrt(dxLocker * dxLocker + dyLocker * dyLocker);
if (distLocker < 100 && !locker.isOccupied) {
nearLocker = true;
lastTouchedLocker = locker;
// Double tap or tap when monster is chasing to hide
if (isDragging && (monster.isChasing || LK.ticks % 30 === 0)) {
player.hide();
player.x = locker.x;
player.y = locker.y;
locker.isOccupied = true;
break;
}
}
}
// Allow player to exit locker when not being chased
if (player.isHidden && !monster.isChasing && isDragging) {
player.unhide();
if (lastTouchedLocker) {
lastTouchedLocker.isOccupied = false;
}
}
// Check for reaching an exit in escape phase
if (gamePhase === "escaping") {
for (var k = 0; k < exits.length; k++) {
var exit = exits[k];
if (player.intersects(exit) && !player.isHidden) {
// Check if this is the last exit (the only open one)
if (k === exits.length - 1) {
// Play escape cutscene
player.isHidden = true; // Prevent further interaction
monster.isChasing = true;
monster.targetX = player.x;
monster.targetY = player.y;
// Player slides through the exit
tween(player, {
x: exit.x,
y: exit.y,
alpha: 0.5,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
onFinish: function onFinish() {
// Monster gets crushed by the door - dramatic chase sequence
tween(monster, {
x: exit.x - 50,
y: exit.y
}, {
duration: 600,
onFinish: function onFinish() {
// Monster tries to reach player in last desperate attempt
tween(monster.children[0], {
scaleX: 1.3,
scaleY: 1.1,
tint: 0xFF0000
}, {
duration: 300,
onFinish: function onFinish() {
// Door closes on monster
LK.effects.flashScreen(0xFF0000, 500);
// Play monster detection sound for impact
LK.getSound('monster_detect').play();
// Flatten monster to show crushing
tween(monster.children[0], {
scaleY: 0.2,
scaleX: 1.8,
alpha: 0.5,
tint: 0x660000
}, {
duration: 700,
onFinish: function onFinish() {
// Show "soft place center" message
instructionText.setText("You've escaped to the soft place center! Chapter complete.");
instructionText.style.fill = "#00FF00";
// Create final escape visual effect
var light = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: exit.x,
y: exit.y,
alpha: 0,
width: 50,
height: 50,
tint: 0xFFFFFF
});
game.addChild(light);
tween(light, {
alpha: 0.8,
width: 2048,
height: 2048
}, {
duration: 1500,
onFinish: function onFinish() {
LK.setTimeout(function () {
LK.showYouWin();
}, 500);
}
});
}
});
}
});
}
});
}
});
return;
} else {
// Closed exit - monster gets angrier
LK.effects.flashScreen(0xFF0000, 300);
monster.speed += 0.5;
monster.startChasing(player.x, player.y);
}
}
}
}
// Monster detection
var dxMonster = monster.x - player.x;
var dyMonster = monster.y - player.y;
var distMonster = Math.sqrt(dxMonster * dxMonster + dyMonster * dyMonster);
// Proximity warning - monster is getting close
if (!player.isHidden && distMonster < monster.detectionRadius * 1.5 && !monster.isChasing) {
// Visual proximity indicator
if (LK.ticks % 60 === 0) {
LK.effects.flashScreen(0x330000, 300);
// Increase player fear based on proximity
var proximityFear = 1 - distMonster / (monster.detectionRadius * 1.5);
player.makeNoise(proximityFear * 0.1); // Fear increases noise level slightly
// Heartbeat effect - the closer the monster, the faster the heartbeat
var heartbeatIntensity = 1 - distMonster / (monster.detectionRadius * 1.5);
// Pulse the player slightly to simulate heartbeat
tween(player, {
scaleX: 1 + heartbeatIntensity * 0.1,
scaleY: 1 + heartbeatIntensity * 0.1
}, {
duration: 300 - heartbeatIntensity * 200,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 300 - heartbeatIntensity * 200
});
}
});
}
}
// Monster detects player based on noise
if (!player.isHidden && distMonster < monster.detectionRadius * player.noiseLevel) {
monster.startChasing(player.x, player.y);
// Show warning message as instructed
instructionText.setText('RUN AND GO FIND A LOCKER!!');
instructionText.style.fill = "#FF0000";
}
// Monster can see player in the final phase
else if (gamePhase === "escaping" && monster.hasSight && !player.isHidden && distMonster < monster.detectionRadius * 1.5) {
monster.startChasing(player.x, player.y);
}
// Update monster's target position if already chasing
else if (monster.isChasing && !player.isHidden) {
monster.targetX = player.x;
monster.targetY = player.y;
}
// Stop chasing if player is hidden
else if (player.isHidden && monster.isChasing) {
LK.setTimeout(function () {
monster.stopChasing();
}, 1500);
}
// Stop chasing if lost track for too long
else if (monster.isChasing && LK.ticks % 180 === 0) {
monster.stopChasing();
}
// Check for collisions with walls
function checkWallCollisions() {
// Check player-wall collisions
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (player.intersects(wall) && !player.isHidden) {
// Calculate push back direction
var dx = player.x - wall.x;
var dy = player.y - wall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// Push player away from wall
if (dist > 0) {
// Calculate minimal push needed
var overlapX = (player.width + wall.width) / 2 - Math.abs(dx);
var overlapY = (player.height + wall.height) / 2 - Math.abs(dy);
// Push in the direction of minimum overlap
if (overlapX < overlapY) {
player.x += dx / dist * overlapX * 1.1;
} else {
player.y += dy / dist * overlapY * 1.1;
}
}
}
}
// Check monster-wall collisions (less strict, monster can navigate around walls better)
if (monster.isChasing) {
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (monster.intersects(wall)) {
// Less strict collision for monster - allows some navigation through gaps
var dx = monster.x - wall.x;
var dy = monster.y - wall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
monster.x += dx / dist * 2;
monster.y += dy / dist * 2;
}
}
}
}
}
checkWallCollisions();
// Update darkness mask to follow player
for (var i = 0; i < game.children.length; i++) {
if (game.children[i] instanceof DarknessMask) {
game.children[i].updatePosition(player.x, player.y);
// Also adjust light radius based on player noise
game.children[i].adjustLightRadius(player.noiseLevel);
break;
}
}
// Check for player caught by monster
if (!player.isHidden && player.intersects(monster)) {
// Flash the screen red
LK.effects.flashScreen(0xFF0000, 500);
// Game over
LK.showGameOver();
}
};
Make a mascot evil creature. In-Game asset. 2d. High contrast. No shadows
A dirty notebook like it was in a abandoned school. In-Game asset. 2d. High contrast. No shadows
Make a scary dark locker that is blue and make it like it was in a abandoned school. In-Game asset. 2d. High contrast. No shadows
Make a wall like it was in a abandoned school. In-Game asset. 2d. High contrast. No shadows
Make a door with a white wall like it was in an abandoned school on top of the door is a glowing red exit sign. In-Game asset. 2d. High contrast. No shadows
Make a centercircle in an abandoned school. In-Game asset. 2d. High contrast. No shadows