Spaces:
Running
Running

itle: School Escape — The Great Bunk Mission Story Introduction: Four best friends — Aryan (the leader), Ravi, Sid, and Karan (the main character) — plan to bunk school and go on an adventure. Aryan, Ravi, and Sid convince Karan to skip class. Karan agrees, but since he’s on the 10th floor, it won’t be easy. The other three friends wait for him outside, ready with two bikes to escape together. However, Karan will have to navigate all 10 floors of the school, overcoming obstacles, avoiding teachers, sneaking past the principal, and dealing with the strict security guard to make it to freedom. --- LEVEL STRUCTURE (10 Levels) Level 1 (10th Floor - Classroom Escape): Objective: Leave class without being seen by the strict subject teacher. Gameplay: Dodge the teacher’s patrol route, collect a hall pass to fool any passing teacher. --- Level 2 (9th Floor - Science Lab Chaos): Objective: Cross the science lab where chemicals and lab equipment block the way. Gameplay: Solve a simple puzzle to move tables, avoid the lab assistant, and not break any glass beakers. --- Level 3 (8th Floor - Corridor Patrol): Objective: Get past the floor’s corridor monitor who will report to the principal. Gameplay: Hide behind notice boards, sneak through open lockers to stay out of sight. --- Level 4 (7th Floor - Library Stealth): Objective: Cross the library without making noise or alerting the librarian. Gameplay: Step only on the blue tiles (non-creaky ones), and hide behind bookshelves if the librarian looks around. --- Level 5 (6th Floor - PT Sir Challenge): Objective: Get past the PT Sir doing rounds. Gameplay: Dodge his whistle sound zones and jump over hurdles he placed as punishment for late students. --- Level 6 (5th Floor - Art Room Obstacle): Objective: Escape through the messy art room where paint buckets and brushes are strewn about. Gameplay: Avoid slipping on wet paint and push easels to create a path. --- Level 7 (4th Floor - Principal's Office Danger): Objective: Avoid getting caught outside the principal’s office. Gameplay: Listen carefully — whenever you hear the principal’s footsteps, hide immediately. --- Level 8 (3rd Floor - Canteen Blockade): Objective: Get through the canteen where your class teacher is buying snacks. Gameplay: Distract the teacher by spilling water on the floor so you can slip by. --- Level 9 (2nd Floor - Security Guard Checkpoint): Objective: Bypass the guard who blocks the main stairs. Gameplay: Collect a fake permission slip to fool the guard, then slip down to the ground floor. --- Level 10 (1st Floor - Final Escape): Objective: The gate is locked, and the guard won’t let you leave. Karan gets an idea to go to the school terrace. There’s a neighboring house close to the school building. Gameplay: Climb to the terrace, find a plank, balance it between the school and the neighbor’s house, and cross carefully. Ending Cutscene Karan successfully reaches the house’s terrace, climbs down, and meets Aryan, Sid, and Ravi. They ride away together on two bikes, laughing and cheering as they escape for their adventure. --- Extra Twist: Add bonus collectible items on each level (like keys, notes, school ID cards) to unlock secret costumes or power-ups. Add a timer for speed challenges. make a game for me and noise - Initial Deployment
cb998b2
verified
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>School Escape — The Great Bunk Mission</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
@keyframes pulse { | |
0%, 100% { transform: scale(1); } | |
50% { transform: scale(1.05); } | |
} | |
.pulse-animation { | |
animation: pulse 2s infinite; | |
} | |
.game-container { | |
background-size: cover; | |
background-position: center; | |
transition: background-image 0.5s ease; | |
} | |
.player { | |
transition: all 0.3s ease; | |
z-index: 10; | |
} | |
.obstacle { | |
z-index: 5; | |
} | |
.collectible { | |
z-index: 7; | |
animation: float 2s ease-in-out infinite; | |
} | |
@keyframes float { | |
0%, 100% { transform: translateY(0); } | |
50% { transform: translateY(-10px); } | |
} | |
.progress-bar { | |
transition: width 0.3s ease; | |
} | |
.modal { | |
transition: opacity 0.3s ease, transform 0.3s ease; | |
} | |
.modal-hidden { | |
opacity: 0; | |
transform: translateY(-20px); | |
pointer-events: none; | |
} | |
.noise-meter { | |
transition: width 0.1s linear; | |
} | |
.level-complete { | |
animation: celebrate 1s ease infinite; | |
} | |
@keyframes celebrate { | |
0%, 100% { transform: translateY(0) rotate(0); } | |
25% { transform: translateY(-10px) rotate(5deg); } | |
50% { transform: translateY(0) rotate(0); } | |
75% { transform: translateY(-10px) rotate(-5deg); } | |
} | |
</style> | |
</head> | |
<body class="bg-gray-900 text-white font-sans"> | |
<!-- Main Menu --> | |
<div id="main-menu" class="fixed inset-0 flex flex-col items-center justify-center bg-gray-900 z-50"> | |
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl max-w-md w-full mx-4 text-center"> | |
<h1 class="text-4xl font-bold mb-2 text-yellow-400">School Escape</h1> | |
<h2 class="text-xl mb-6 text-yellow-300">The Great Bunk Mission</h2> | |
<p class="mb-6 text-gray-300">Help Karan escape school through 10 challenging floors to meet his friends for an epic adventure!</p> | |
<button id="start-game" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-full mb-4 w-full transition-all transform hover:scale-105"> | |
Start Adventure | |
</button> | |
<button id="how-to-play" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-full mb-4 w-full transition-all transform hover:scale-105"> | |
How to Play | |
</button> | |
<button id="credits-btn" class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-6 rounded-full w-full transition-all transform hover:scale-105"> | |
Credits | |
</button> | |
</div> | |
</div> | |
<!-- How to Play Modal --> | |
<div id="how-to-play-modal" class="modal fixed inset-0 flex items-center justify-center z-50 modal-hidden"> | |
<div class="bg-gray-800 p-6 rounded-lg shadow-2xl max-w-md w-full mx-4"> | |
<h2 class="text-2xl font-bold mb-4 text-yellow-400">How to Play</h2> | |
<div class="space-y-4 text-gray-300"> | |
<p><span class="font-bold text-yellow-300">Objective:</span> Guide Karan through 10 school floors to escape and meet his friends.</p> | |
<p><span class="font-bold text-yellow-300">Controls:</span> Use arrow keys or WASD to move. Space to interact.</p> | |
<p><span class="font-bold text-yellow-300">Noise Meter:</span> Running makes noise! Keep it low to avoid detection.</p> | |
<p><span class="font-bold text-yellow-300">Collectibles:</span> Find hall passes, keys, and other items to help your escape.</p> | |
<p><span class="font-bold text-yellow-300">Timer:</span> Complete each level quickly for bonus points!</p> | |
</div> | |
<button id="close-how-to-play" class="mt-6 bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full w-full transition-all"> | |
Back to Menu | |
</button> | |
</div> | |
</div> | |
<!-- Credits Modal --> | |
<div id="credits-modal" class="modal fixed inset-0 flex items-center justify-center z-50 modal-hidden"> | |
<div class="bg-gray-800 p-6 rounded-lg shadow-2xl max-w-md w-full mx-4"> | |
<h2 class="text-2xl font-bold mb-4 text-yellow-400">Credits</h2> | |
<div class="space-y-3 text-gray-300"> | |
<p><span class="font-bold text-yellow-300">Game Concept:</span> School Escape Adventure</p> | |
<p><span class="font-bold text-yellow-300">Developed by:</span> Your Game Studio</p> | |
<p><span class="font-bold text-yellow-300">Special Thanks:</span> All the students who ever dreamed of bunking school</p> | |
<p><span class="font-bold text-yellow-300">Tools Used:</span> HTML, CSS, JavaScript, TailwindCSS</p> | |
</div> | |
<button id="close-credits" class="mt-6 bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full w-full transition-all"> | |
Back to Menu | |
</button> | |
</div> | |
</div> | |
<!-- Game UI --> | |
<div id="game-ui" class="fixed top-0 left-0 right-0 p-4 flex justify-between items-center z-40 hidden"> | |
<div class="flex items-center space-x-4"> | |
<div class="bg-gray-800 bg-opacity-80 rounded-full p-2"> | |
<span id="level-display" class="font-bold text-yellow-400">Floor 10</span> | |
</div> | |
<div class="bg-gray-800 bg-opacity-80 rounded-full p-2"> | |
<span id="timer" class="font-mono">00:00</span> | |
</div> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<div class="bg-gray-800 bg-opacity-80 rounded-full p-2 flex items-center"> | |
<i class="fas fa-volume-up text-yellow-400 mr-2"></i> | |
<div class="w-24 h-2 bg-gray-700 rounded-full overflow-hidden"> | |
<div id="noise-meter" class="noise-meter h-full bg-red-500" style="width: 0%"></div> | |
</div> | |
</div> | |
<div class="bg-gray-800 bg-opacity-80 rounded-full p-2"> | |
<span id="collectibles-count" class="font-bold">0/3</span> | |
</div> | |
</div> | |
</div> | |
<!-- Pause Menu --> | |
<div id="pause-menu" class="fixed inset-0 flex items-center justify-center z-40 hidden"> | |
<div class="bg-gray-800 bg-opacity-90 p-8 rounded-lg shadow-2xl max-w-md w-full mx-4 text-center"> | |
<h2 class="text-3xl font-bold mb-6 text-yellow-400">Game Paused</h2> | |
<button id="resume-game" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-full mb-4 w-full transition-all transform hover:scale-105"> | |
Resume Game | |
</button> | |
<button id="restart-level" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-full mb-4 w-full transition-all transform hover:scale-105"> | |
Restart Level | |
</button> | |
<button id="quit-to-menu" class="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-full w-full transition-all transform hover:scale-105"> | |
Quit to Menu | |
</button> | |
</div> | |
</div> | |
<!-- Level Complete Modal --> | |
<div id="level-complete-modal" class="modal fixed inset-0 flex items-center justify-center z-50 modal-hidden"> | |
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl max-w-md w-full mx-4 text-center"> | |
<h2 class="text-3xl font-bold mb-4 text-green-400 level-complete">Level Complete!</h2> | |
<div class="mb-6"> | |
<p class="text-xl mb-2">Time: <span id="complete-time" class="font-bold">00:00</span></p> | |
<p class="text-xl mb-2">Collectibles: <span id="complete-collectibles" class="font-bold">0/3</span></p> | |
<p class="text-xl">Score: <span id="complete-score" class="font-bold text-yellow-400">0</span></p> | |
</div> | |
<button id="next-level" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-full w-full transition-all transform hover:scale-105"> | |
Continue to Next Floor | |
</button> | |
</div> | |
</div> | |
<!-- Game Over Modal --> | |
<div id="game-over-modal" class="modal fixed inset-0 flex items-center justify-center z-50 modal-hidden"> | |
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl max-w-md w-full mx-4 text-center"> | |
<h2 class="text-3xl font-bold mb-4 text-red-400">Caught!</h2> | |
<p class="text-xl mb-6">You've been detected by school authorities!</p> | |
<button id="retry-level" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-full mb-4 w-full transition-all transform hover:scale-105"> | |
Try Again | |
</button> | |
<button id="game-over-to-menu" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-full w-full transition-all transform hover:scale-105"> | |
Back to Menu | |
</button> | |
</div> | |
</div> | |
<!-- Game Container --> | |
<div id="game-container" class="game-container fixed inset-0 overflow-hidden hidden"> | |
<!-- Player --> | |
<div id="player" class="player absolute w-12 h-12 bg-blue-600 rounded-full flex items-center justify-center shadow-lg"> | |
<i class="fas fa-user text-white"></i> | |
</div> | |
<!-- Obstacles will be added dynamically --> | |
<!-- Collectibles will be added dynamically --> | |
<!-- Exit will be added dynamically --> | |
<!-- Level-specific elements will be added dynamically --> | |
</div> | |
<!-- Story Intro Modal --> | |
<div id="story-intro-modal" class="modal fixed inset-0 flex items-center justify-center z-50 hidden"> | |
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl max-w-2xl w-full mx-4"> | |
<h2 class="text-3xl font-bold mb-4 text-yellow-400">The Great Bunk Mission</h2> | |
<div class="text-gray-300 mb-6 space-y-4"> | |
<p>Four best friends — Aryan (the leader), Ravi, Sid, and Karan (you) — plan to bunk school and go on an adventure.</p> | |
<p>Aryan, Ravi, and Sid convince you to skip class. You agree, but since you're on the 10th floor, it won't be easy.</p> | |
<p>The other three friends wait for you outside, ready with two bikes to escape together.</p> | |
<p>But first, you'll have to navigate all 10 floors of the school, overcoming obstacles, avoiding teachers, sneaking past the principal, and dealing with the strict security guard to make it to freedom.</p> | |
<p class="font-bold text-yellow-300">Good luck! Don't get caught!</p> | |
</div> | |
<button id="start-first-level" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-full w-full transition-all transform hover:scale-105"> | |
Begin Escape | |
</button> | |
</div> | |
</div> | |
<!-- Level Intro Modal --> | |
<div id="level-intro-modal" class="modal fixed inset-0 flex items-center justify-center z-50 hidden"> | |
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl max-w-md w-full mx-4"> | |
<h2 id="level-intro-title" class="text-2xl font-bold mb-2 text-yellow-400">Floor 10: Classroom Escape</h2> | |
<h3 id="level-intro-subtitle" class="text-lg mb-4 text-yellow-300">Leave class without being seen</h3> | |
<div id="level-intro-description" class="text-gray-300 mb-6"> | |
<p>Dodge the teacher's patrol route and collect a hall pass to fool any passing teacher.</p> | |
<p class="mt-2 text-sm text-yellow-200">Tip: Walk slowly (hold Shift) to make less noise.</p> | |
</div> | |
<button id="start-level" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-full w-full transition-all transform hover:scale-105"> | |
Start Level | |
</button> | |
</div> | |
</div> | |
<!-- Game Complete Modal --> | |
<div id="game-complete-modal" class="modal fixed inset-0 flex items-center justify-center z-50 modal-hidden"> | |
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl max-w-2xl w-full mx-4 text-center"> | |
<h2 class="text-4xl font-bold mb-4 text-green-400">Escape Successful!</h2> | |
<div class="mb-6"> | |
<p class="text-xl mb-4">You made it! Karan successfully reaches the house's terrace, climbs down, and meets Aryan, Sid, and Ravi.</p> | |
<p class="text-xl mb-4">They ride away together on two bikes, laughing and cheering as they escape for their adventure.</p> | |
<div class="flex justify-center my-6"> | |
<div class="bg-yellow-600 text-white p-4 rounded-full mx-2"><i class="fas fa-user text-2xl"></i></div> | |
<div class="bg-blue-600 text-white p-4 rounded-full mx-2"><i class="fas fa-user text-2xl"></i></div> | |
<div class="bg-green-600 text-white p-4 rounded-full mx-2"><i class="fas fa-user text-2xl"></i></div> | |
<div class="bg-red-600 text-white p-4 rounded-full mx-2"><i class="fas fa-user text-2xl"></i></div> | |
</div> | |
<p class="text-lg">Total Score: <span id="final-score" class="font-bold text-yellow-400">0</span></p> | |
<p class="text-lg">Total Collectibles: <span id="final-collectibles" class="font-bold">0/30</span></p> | |
</div> | |
<button id="play-again" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-full mb-4 w-full transition-all transform hover:scale-105"> | |
Play Again | |
</button> | |
<button id="complete-to-menu" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-full w-full transition-all transform hover:scale-105"> | |
Back to Menu | |
</button> | |
</div> | |
</div> | |
<script> | |
// Game state | |
const gameState = { | |
currentLevel: 1, | |
totalLevels: 10, | |
score: 0, | |
totalCollectibles: 0, | |
collectedCollectibles: 0, | |
levelCollectibles: 0, | |
time: 0, | |
timerInterval: null, | |
noiseLevel: 0, | |
isMoving: false, | |
isSneaking: false, | |
gameActive: false, | |
player: { | |
x: 0, | |
y: 0, | |
width: 48, | |
height: 48, | |
speed: 3, | |
sneakingSpeed: 1.5 | |
}, | |
keys: { | |
up: false, | |
down: false, | |
left: false, | |
right: false, | |
shift: false | |
}, | |
levels: [ | |
{ | |
title: "Floor 10: Classroom Escape", | |
subtitle: "Leave class without being seen", | |
description: "Dodge the teacher's patrol route and collect a hall pass to fool any passing teacher.", | |
tip: "Tip: Walk slowly (hold Shift) to make less noise.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #4b5563 0%, #1f2937 100%)", | |
obstacles: [ | |
{ x: 100, y: 100, width: 200, height: 20, type: "desk" }, | |
{ x: 400, y: 200, width: 20, height: 300, type: "bookshelf" }, | |
{ x: 200, y: 400, width: 300, height: 20, type: "desk" }, | |
{ x: 100, y: 300, width: 150, height: 20, type: "desk" } | |
], | |
enemies: [ | |
{ x: 300, y: 150, width: 40, height: 60, type: "teacher", patrol: [{x: 300, y: 150}, {x: 500, y: 150}, {x: 500, y: 350}, {x: 300, y: 350}] } | |
], | |
collectiblePositions: [ | |
{ x: 150, y: 250, type: "hall-pass" }, | |
{ x: 450, y: 300, type: "key" }, | |
{ x: 250, y: 450, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 9: Science Lab Chaos", | |
subtitle: "Cross the dangerous science lab", | |
description: "Solve a simple puzzle to move tables, avoid the lab assistant, and don't break any glass beakers.", | |
tip: "Tip: Some obstacles can be moved by pushing them (press Space when nearby).", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)", | |
obstacles: [ | |
{ x: 150, y: 100, width: 100, height: 60, type: "table", movable: true }, | |
{ x: 300, y: 200, width: 120, height: 60, type: "table", movable: true }, | |
{ x: 200, y: 350, width: 100, height: 60, type: "table", movable: true }, | |
{ x: 450, y: 100, width: 20, height: 400, type: "cabinet" } | |
], | |
enemies: [ | |
{ x: 400, y: 300, width: 40, height: 60, type: "lab-assistant", patrol: [{x: 400, y: 300}, {x: 400, y: 150}, {x: 600, y: 150}, {x: 600, y: 300}] } | |
], | |
collectiblePositions: [ | |
{ x: 200, y: 200, type: "beaker" }, | |
{ x: 350, y: 250, type: "key" }, | |
{ x: 500, y: 200, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 8: Corridor Patrol", | |
subtitle: "Avoid the corridor monitor", | |
description: "Hide behind notice boards, sneak through open lockers to stay out of sight of the corridor monitor.", | |
tip: "Tip: Some objects provide hiding spots when you stand near them.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #6b7280 0%, #4b5563 100%)", | |
obstacles: [ | |
{ x: 100, y: 100, width: 50, height: 150, type: "locker", hideable: true }, | |
{ x: 200, y: 200, width: 100, height: 20, type: "notice-board", hideable: true }, | |
{ x: 350, y: 150, width: 50, height: 150, type: "locker", hideable: true }, | |
{ x: 450, y: 300, width: 100, height: 20, type: "notice-board", hideable: true }, | |
{ x: 550, y: 100, width: 50, height: 150, type: "locker", hideable: true } | |
], | |
enemies: [ | |
{ x: 300, y: 400, width: 40, height: 60, type: "monitor", patrol: [{x: 100, y: 400}, {x: 600, y: 400}] } | |
], | |
collectiblePositions: [ | |
{ x: 150, y: 300, type: "id-card" }, | |
{ x: 400, y: 250, type: "key" }, | |
{ x: 550, y: 300, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 7: Library Stealth", | |
subtitle: "Cross without alerting the librarian", | |
description: "Step only on the blue tiles (non-creaky ones), and hide behind bookshelves if the librarian looks around.", | |
tip: "Tip: The noise meter fills faster when stepping on creaky floor tiles.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #a16207 0%, #713f12 100%)", | |
obstacles: [ | |
{ x: 100, y: 100, width: 100, height: 300, type: "bookshelf", hideable: true }, | |
{ x: 250, y: 150, width: 100, height: 250, type: "bookshelf", hideable: true }, | |
{ x: 400, y: 100, width: 100, height: 300, type: "bookshelf", hideable: true }, | |
{ x: 550, y: 150, width: 100, height: 250, type: "bookshelf", hideable: true } | |
], | |
creakyTiles: [ | |
{ x: 200, y: 100, width: 50, height: 50 }, | |
{ x: 350, y: 150, width: 50, height: 50 }, | |
{ x: 200, y: 200, width: 50, height: 50 }, | |
{ x: 350, y: 250, width: 50, height: 50 }, | |
{ x: 200, y: 300, width: 50, height: 50 }, | |
{ x: 500, y: 100, width: 50, height: 50 }, | |
{ x: 500, y: 200, width: 50, height: 50 }, | |
{ x: 500, y: 300, width: 50, height: 50 } | |
], | |
enemies: [ | |
{ x: 300, y: 400, width: 40, height: 60, type: "librarian", patrol: [{x: 100, y: 400}, {x: 600, y: 400}], detectionRange: 150 } | |
], | |
collectiblePositions: [ | |
{ x: 150, y: 200, type: "book" }, | |
{ x: 300, y: 250, type: "key" }, | |
{ x: 450, y: 200, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 6: PT Sir Challenge", | |
subtitle: "Get past the strict PT teacher", | |
description: "Dodge his whistle sound zones and jump over hurdles he placed as punishment for late students.", | |
tip: "Tip: Time your movements to avoid the whistle zones.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #10b981 0%, #047857 100%)", | |
obstacles: [ | |
{ x: 150, y: 200, width: 100, height: 20, type: "hurdle" }, | |
{ x: 300, y: 300, width: 100, height: 20, type: "hurdle" }, | |
{ x: 450, y: 200, width: 100, height: 20, type: "hurdle" }, | |
{ x: 600, y: 300, width: 100, height: 20, type: "hurdle" } | |
], | |
whistleZones: [ | |
{ x: 200, y: 100, width: 200, height: 50, active: true, timer: 0, interval: 120 }, | |
{ x: 400, y: 100, width: 200, height: 50, active: false, timer: 60, interval: 120 } | |
], | |
enemies: [ | |
{ x: 300, y: 400, width: 60, height: 80, type: "pt-teacher", patrol: [{x: 200, y: 400}, {x: 500, y: 400}] } | |
], | |
collectiblePositions: [ | |
{ x: 200, y: 250, type: "whistle" }, | |
{ x: 400, y: 250, type: "key" }, | |
{ x: 600, y: 250, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 5: Art Room Obstacle", | |
subtitle: "Escape through the messy art room", | |
description: "Avoid slipping on wet paint and push easels to create a path through the cluttered room.", | |
tip: "Tip: Wet paint slows you down and makes more noise when you walk through it.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #d946ef 0%, #a21caf 100%)", | |
obstacles: [ | |
{ x: 150, y: 150, width: 80, height: 120, type: "easel", movable: true }, | |
{ x: 300, y: 200, width: 80, height: 120, type: "easel", movable: true }, | |
{ x: 450, y: 150, width: 80, height: 120, type: "easel", movable: true }, | |
{ x: 600, y: 200, width: 80, height: 120, type: "easel", movable: true } | |
], | |
paintSpills: [ | |
{ x: 200, y: 300, width: 100, height: 50 }, | |
{ x: 400, y: 350, width: 100, height: 50 }, | |
{ x: 550, y: 300, width: 100, height: 50 } | |
], | |
enemies: [ | |
{ x: 350, y: 400, width: 40, height: 60, type: "art-teacher", patrol: [{x: 200, y: 400}, {x: 500, y: 400}] } | |
], | |
collectiblePositions: [ | |
{ x: 200, y: 200, type: "paintbrush" }, | |
{ x: 400, y: 200, type: "key" }, | |
{ x: 600, y: 200, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 4: Principal's Office Danger", | |
subtitle: "Avoid getting caught outside the office", | |
description: "Listen carefully — whenever you hear the principal's footsteps, hide immediately.", | |
tip: "Tip: The principal has a pattern - watch and learn before moving.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)", | |
obstacles: [ | |
{ x: 100, y: 150, width: 100, height: 60, type: "plant", hideable: true }, | |
{ x: 250, y: 200, width: 100, height: 60, type: "bench", hideable: true }, | |
{ x: 400, y: 150, width: 100, height: 60, type: "plant", hideable: true }, | |
{ x: 550, y: 200, width: 100, height: 60, type: "bench", hideable: true } | |
], | |
enemies: [ | |
{ | |
x: 350, y: 400, | |
width: 50, height: 70, | |
type: "principal", | |
patrol: [ | |
{x: 200, y: 400}, | |
{x: 500, y: 400} | |
], | |
pattern: [ | |
{ move: true, duration: 120 }, | |
{ move: false, duration: 60, alert: "Footsteps approaching!" }, | |
{ move: true, duration: 120 }, | |
{ move: false, duration: 90, alert: "Listening for noise..." } | |
], | |
currentPatternStep: 0, | |
patternTimer: 0 | |
} | |
], | |
collectiblePositions: [ | |
{ x: 150, y: 250, type: "trophy" }, | |
{ x: 400, y: 250, type: "key" }, | |
{ x: 650, y: 250, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 3: Canteen Blockade", | |
subtitle: "Get through the canteen undetected", | |
description: "Your class teacher is buying snacks. Distract them by spilling water on the floor so you can slip by.", | |
tip: "Tip: Find the water bottle first to create a distraction.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #f59e0b 0%, #b45309 100%)", | |
obstacles: [ | |
{ x: 150, y: 150, width: 100, height: 60, type: "table" }, | |
{ x: 300, y: 200, width: 100, height: 60, type: "table" }, | |
{ x: 450, y: 150, width: 100, height: 60, type: "table" }, | |
{ x: 600, y: 200, width: 100, height: 60, type: "table" } | |
], | |
enemies: [ | |
{ | |
x: 400, y: 300, | |
width: 40, height: 60, | |
type: "class-teacher", | |
patrol: [{x: 400, y: 300}], | |
distracted: false, | |
distractionTimer: 0 | |
} | |
], | |
collectiblePositions: [ | |
{ x: 200, y: 200, type: "water-bottle", special: true }, | |
{ x: 400, y: 200, type: "key" }, | |
{ x: 600, y: 200, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 2: Security Guard Checkpoint", | |
subtitle: "Bypass the strict security guard", | |
description: "Collect a fake permission slip to fool the guard, then slip down to the ground floor.", | |
tip: "Tip: The guard has a limited field of vision - stay out of it.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #64748b 0%, #475569 100%)", | |
obstacles: [ | |
{ x: 300, y: 200, width: 100, height: 60, type: "desk" }, | |
{ x: 450, y: 300, width: 100, height: 60, type: "chair" } | |
], | |
enemies: [ | |
{ | |
x: 350, y: 400, | |
width: 50, height: 70, | |
type: "security-guard", | |
patrol: [{x: 350, y: 400}], | |
visionCone: [ | |
{x: 350, y: 400}, | |
{x: 250, y: 300}, | |
{x: 450, y: 300} | |
], | |
hasPermission: false | |
} | |
], | |
collectiblePositions: [ | |
{ x: 200, y: 250, type: "permission-slip", special: true }, | |
{ x: 400, y: 250, type: "key" }, | |
{ x: 600, y: 250, type: "note" } | |
], | |
exit: { x: 700, y: 500, width: 60, height: 80, type: "door" }, | |
playerStart: { x: 50, y: 50 } | |
}, | |
{ | |
title: "Floor 1: Final Escape", | |
subtitle: "Find an alternative way out", | |
description: "The gate is locked, and the guard won't let you leave. Time to get creative - head to the terrace!", | |
tip: "Tip: Find the plank to create a bridge to the neighboring house.", | |
collectibles: 3, | |
background: "linear-gradient(135deg, #0ea5e9 0%, #0369a1 100%)", | |
obstacles: [ | |
{ x: 100, y: 150, width: 100, height: 60, type: "locker" }, | |
{ x: 250, y: 200, width: 100, height: 60, type: "bench" }, | |
{ x: 400, y: 150, width: 100, height: 60, type: "locker" }, | |
{ x: 550, y: 200, width: 100, height: 60, type: "bench" }, | |
{ x: 650, y: 100, width: 40, height: 300, type: "stairs" } | |
], | |
enemies: [ | |
{ | |
x: 400, y: 400, | |
width: 50, height: 70, | |
type: "final-guard", | |
patrol: [{x: 300, y: 400}, {x: 500, y: 400}], | |
detectionRange: 200 | |
} | |
], | |
collectiblePositions: [ | |
{ x: 200, y: 250, type: "plank", special: true }, | |
{ x: 400, y: 250, type: "key" }, | |
{ x: 600, y: 250, type: "note" } | |
], | |
exit: { x: 700, y: 100, width: 60, height: 80, type: "terrace-door" }, | |
playerStart: { x: 50, y: 50 }, | |
hasPlank: false, | |
bridgeBuilt: false | |
} | |
] | |
}; | |
// DOM elements | |
const elements = { | |
mainMenu: document.getElementById('main-menu'), | |
gameUI: document.getElementById('game-ui'), | |
gameContainer: document.getElementById('game-container'), | |
player: document.getElementById('player'), | |
pauseMenu: document.getElementById('pause-menu'), | |
levelCompleteModal: document.getElementById('level-complete-modal'), | |
gameOverModal: document.getElementById('game-over-modal'), | |
storyIntroModal: document.getElementById('story-intro-modal'), | |
levelIntroModal: document.getElementById('level-intro-modal'), | |
gameCompleteModal: document.getElementById('game-complete-modal'), | |
howToPlayModal: document.getElementById('how-to-play-modal'), | |
creditsModal: document.getElementById('credits-modal'), | |
levelDisplay: document.getElementById('level-display'), | |
timer: document.getElementById('timer'), | |
noiseMeter: document.getElementById('noise-meter'), | |
collectiblesCount: document.getElementById('collectibles-count'), | |
completeTime: document.getElementById('complete-time'), | |
completeCollectibles: document.getElementById('complete-collectibles'), | |
completeScore: document.getElementById('complete-score'), | |
finalScore: document.getElementById('final-score'), | |
finalCollectibles: document.getElementById('final-collectibles') | |
}; | |
// Buttons | |
const buttons = { | |
startGame: document.getElementById('start-game'), | |
howToPlay: document.getElementById('how-to-play'), | |
creditsBtn: document.getElementById('credits-btn'), | |
closeHowToPlay: document.getElementById('close-how-to-play'), | |
closeCredits: document.getElementById('close-credits'), | |
resumeGame: document.getElementById('resume-game'), | |
restartLevel: document.getElementById('restart-level'), | |
quitToMenu: document.getElementById('quit-to-menu'), | |
nextLevel: document.getElementById('next-level'), | |
retryLevel: document.getElementById('retry-level'), | |
gameOverToMenu: document.getElementById('game-over-to-menu'), | |
startFirstLevel: document.getElementById('start-first-level'), | |
startLevel: document.getElementById('start-level'), | |
playAgain: document.getElementById('play-again'), | |
completeToMenu: document.getElementById('complete-to-menu') | |
}; | |
// Event listeners for buttons | |
buttons.startGame.addEventListener('click', () => { | |
elements.mainMenu.classList.add('hidden'); | |
elements.storyIntroModal.classList.remove('hidden'); | |
}); | |
buttons.howToPlay.addEventListener('click', () => { | |
elements.howToPlayModal.classList.remove('modal-hidden'); | |
}); | |
buttons.creditsBtn.addEventListener('click', () => { | |
elements.creditsModal.classList.remove('modal-hidden'); | |
}); | |
buttons.closeHowToPlay.addEventListener('click', () => { | |
elements.howToPlayModal.classList.add('modal-hidden'); | |
}); | |
buttons.closeCredits.addEventListener('click', () => { | |
elements.creditsModal.classList.add('modal-hidden'); | |
}); | |
buttons.resumeGame.addEventListener('click', resumeGame); | |
buttons.restartLevel.addEventListener('click', restartLevel); | |
buttons.quitToMenu.addEventListener('click', quitToMenu); | |
buttons.nextLevel.addEventListener('click', loadNextLevel); | |
buttons.retryLevel.addEventListener('click', restartLevel); | |
buttons.gameOverToMenu.addEventListener('click', quitToMenu); | |
buttons.startFirstLevel.addEventListener('click', startFirstLevel); | |
buttons.startLevel.addEventListener('click', startLevel); | |
buttons.playAgain.addEventListener('click', () => { | |
elements.gameCompleteModal.classList.add('modal-hidden'); | |
startFirstLevel(); | |
}); | |
buttons.completeToMenu.addEventListener('click', () => { | |
elements.gameCompleteModal.classList.add('modal-hidden'); | |
showMainMenu(); | |
}); | |
// Keyboard controls | |
document.addEventListener('keydown', (e) => { | |
if (!gameState.gameActive) return; | |
switch(e.key) { | |
case 'ArrowUp': | |
case 'w': | |
gameState.keys.up = true; | |
break; | |
case 'ArrowDown': | |
case 's': | |
gameState.keys.down = true; | |
break; | |
case 'ArrowLeft': | |
case 'a': | |
gameState.keys.left = true; | |
break; | |
case 'ArrowRight': | |
case 'd': | |
gameState.keys.right = true; | |
break; | |
case 'Shift': | |
gameState.keys.shift = true; | |
gameState.isSneaking = true; | |
break; | |
case ' ': | |
handleInteraction(); | |
break; | |
case 'Escape': | |
if (elements.pauseMenu.classList.contains('hidden')) { | |
pauseGame(); | |
} else { | |
resumeGame(); | |
} | |
break; | |
} | |
}); | |
document.addEventListener('keyup', (e) => { | |
switch(e.key) { | |
case 'ArrowUp': | |
case 'w': | |
gameState.keys.up = false; | |
break; | |
case 'ArrowDown': | |
case 's': | |
gameState.keys.down = false; | |
break; | |
case 'ArrowLeft': | |
case 'a': | |
gameState.keys.left = false; | |
break; | |
case 'ArrowRight': | |
case 'd': | |
gameState.keys.right = false; | |
break; | |
case 'Shift': | |
gameState.keys.shift = false; | |
gameState.isSneaking = false; | |
break; | |
} | |
}); | |
// Game functions | |
function showMainMenu() { | |
elements.mainMenu.classList.remove('hidden'); | |
elements.gameUI.classList.add('hidden'); | |
elements.gameContainer.classList.add('hidden'); | |
elements.pauseMenu.classList.add('hidden'); | |
stopTimer(); | |
gameState.gameActive = false; | |
} | |
function startFirstLevel() { | |
gameState.currentLevel = 1; | |
gameState.score = 0; | |
gameState.totalCollectibles = 0; | |
elements.storyIntroModal.classList.add('hidden'); | |
loadLevelIntro(); | |
} | |
function loadLevelIntro() { | |
const level = gameState.levels[gameState.currentLevel - 1]; | |
document.getElementById('level-intro-title').textContent = level.title; | |
document.getElementById('level-intro-subtitle').textContent = level.subtitle; | |
document.getElementById('level-intro-description').innerHTML = ` | |
<p>${level.description}</p> | |
<p class="mt-2 text-sm text-yellow-200">${level.tip}</p> | |
`; | |
elements.levelIntroModal.classList.remove('hidden'); | |
} | |
function startLevel() { | |
elements.levelIntroModal.classList.add('hidden'); | |
initializeLevel(); | |
} | |
function initializeLevel() { | |
const level = gameState.levels[gameState.currentLevel - 1]; | |
// Reset level state | |
gameState.collectedCollectibles = 0; | |
gameState.levelCollectibles = level.collectibles; | |
gameState.time = 0; | |
gameState.noiseLevel = 0; | |
gameState.isMoving = false; | |
gameState.isSneaking = false; | |
// Update UI | |
elements.levelDisplay.textContent = `Floor ${11 - gameState.currentLevel}`; | |
elements.collectiblesCount.textContent = `0/${level.collectibles}`; | |
updateTimerDisplay(); | |
updateNoiseMeter(); | |
// Set up game container | |
elements.gameContainer.innerHTML = ''; | |
elements.gameContainer.style.background = level.background; | |
// Position player | |
gameState.player.x = level.playerStart.x; | |
gameState.player.y = level.playerStart.y; | |
updatePlayerPosition(); | |
// Create obstacles | |
level.obstacles.forEach(obstacle => { | |
const obstacleElement = document.createElement('div'); | |
obstacleElement.className = `obstacle absolute ${getObstacleClass(obstacle.type)}`; | |
obstacleElement.style.width = `${obstacle.width}px`; | |
obstacleElement.style.height = `${obstacle.height}px`; | |
obstacleElement.style.left = `${obstacle.x}px`; | |
obstacleElement.style.top = `${obstacle.y}px`; | |
elements.gameContainer.appendChild(obstacleElement); | |
}); | |
// Create creaky tiles if they exist | |
if (level.creakyTiles) { | |
level.creakyTiles.forEach(tile => { | |
const tileElement = document.createElement('div'); | |
tileElement.className = 'absolute bg-blue-900 bg-opacity-50'; | |
tileElement.style.width = `${tile.width}px`; | |
tileElement.style.height = `${tile.height}px`; | |
tileElement.style.left = `${tile.x}px`; | |
tileElement.style.top = `${tile.y}px`; | |
elements.gameContainer.appendChild(tileElement); | |
}); | |
} | |
// Create paint spills if they exist | |
if (level.paintSpills) { | |
level.paintSpills.forEach(paint => { | |
const paintElement = document.createElement('div'); | |
paintElement.className = 'absolute bg-red-400 bg-opacity-70 rounded-full'; | |
paintElement.style.width = `${paint.width}px`; | |
paintElement.style.height = `${paint.height}px`; | |
paintElement.style.left = `${paint.x}px`; | |
paintElement.style.top = `${paint.y}px`; | |
elements.gameContainer.appendChild(paintElement); | |
}); | |
} | |
// Create whistle zones if they exist | |
if (level.whistleZones) { | |
level.whistleZones.forEach(zone => { | |
const zoneElement = document.createElement('div'); | |
zoneElement.className = `absolute border-2 ${zone.active ? 'border-red-500 bg-red-500 bg-opacity-30' : 'border-transparent'}`; | |
zoneElement.style.width = `${zone.width}px`; | |
zoneElement.style.height = `${zone.height}px`; | |
zoneElement.style.left = `${zone.x}px`; | |
zoneElement.style.top = `${zone.y}px`; | |
zoneElement.id = `whistle-zone-${level.whistleZones.indexOf(zone)}`; | |
elements.gameContainer.appendChild(zoneElement); | |
}); | |
} | |
// Create collectibles | |
level.collectiblePositions.forEach(collectible => { | |
const collectibleElement = document.createElement('div'); | |
collectibleElement.className = `collectible absolute ${getCollectibleClass(collectible.type)}`; | |
collectibleElement.style.width = '24px'; | |
collectibleElement.style.height = '24px'; | |
collectibleElement.style.left = `${collectible.x}px`; | |
collectibleElement.style.top = `${collectible.y}px`; | |
collectibleElement.dataset.type = collectible.type; | |
collectibleElement.dataset.special = collectible.special || false; | |
elements.gameContainer.appendChild(collectibleElement); | |
}); | |
// Create exit | |
const exitElement = document.createElement('div'); | |
exitElement.className = `absolute ${getExitClass(level.exit.type)} flex items-center justify-center`; | |
exitElement.style.width = `${level.exit.width}px`; | |
exitElement.style.height = `${level.exit.height}px`; | |
exitElement.style.left = `${level.exit.x}px`; | |
exitElement.style.top = `${level.exit.y}px`; | |
exitElement.innerHTML = getExitIcon(level.exit.type); | |
elements.gameContainer.appendChild(exitElement); | |
// Add player to container | |
elements.gameContainer.appendChild(elements.player); | |
// Show game UI | |
elements.gameUI.classList.remove('hidden'); | |
elements.gameContainer.classList.remove('hidden'); | |
// Start game loop | |
gameState.gameActive = true; | |
startTimer(); | |
requestAnimationFrame(gameLoop); | |
} | |
function getObstacleClass(type) { | |
const classes = { | |
'desk': 'bg-yellow-800', | |
'bookshelf': 'bg-amber-900', | |
'table': 'bg-gray-700', | |
'cabinet': 'bg-blue-900', | |
'locker': 'bg-gray-600', | |
'notice-board': 'bg-white bg-opacity-30', | |
'plant': 'bg-green-700', | |
'bench': 'bg-gray-500', | |
'hurdle': 'bg-red-600', | |
'easel': 'bg-brown-500', | |
'stairs': 'bg-gray-400', | |
'chair': 'bg-gray-600' | |
}; | |
return classes[type] || 'bg-gray-500'; | |
} | |
function getCollectibleClass(type) { | |
const icons = { | |
'hall-pass': 'fas fa-id-card text-yellow-300', | |
'key': 'fas fa-key text-yellow-400', | |
'note': 'fas fa-sticky-note text-white', | |
'beaker': 'fas fa-flask text-blue-300', | |
'id-card': 'fas fa-address-card text-blue-400', | |
'book': 'fas fa-book text-red-300', | |
'whistle': 'fas fa-whistle text-gray-300', | |
'paintbrush': 'fas fa-paint-brush text-pink-400', | |
'trophy': 'fas fa-trophy text-yellow-300', | |
'water-bottle': 'fas fa-bottle-water text-blue-200', | |
'permission-slip': 'fas fa-scroll text-white', | |
'plank': 'fas fa-ruler-combined text-amber-700' | |
}; | |
return icons[type] || 'fas fa-star text-yellow-300'; | |
} | |
function getExitClass(type) { | |
return type === 'terrace-door' ? 'bg-gray-700' : 'bg-brown-600'; | |
} | |
function getExitIcon(type) { | |
return type === 'terrace-door' ? '<i class="fas fa-door-open text-white text-2xl"></i>' : '<i class="fas fa-door-closed text-white text-2xl"></i>'; | |
} | |
function updatePlayerPosition() { | |
elements.player.style.left = `${gameState.player.x}px`; | |
elements.player.style.top = `${gameState.player.y}px`; | |
} | |
function gameLoop() { | |
if (!gameState.gameActive) return; | |
const level = gameState.levels[gameState.currentLevel - 1]; | |
const prevX = gameState.player.x; | |
const prevY = gameState.player.y; | |
// Handle player movement | |
let speed = gameState.isSneaking ? gameState.player.sneakingSpeed : gameState.player.speed; | |
gameState.isMoving = false; | |
if (gameState.keys.up) { | |
gameState.player.y -= speed; | |
gameState.isMoving = true; | |
} | |
if (gameState.keys.down) { | |
gameState.player.y += speed; | |
gameState.isMoving = true; | |
} | |
if (gameState.keys.left) { | |
gameState.player.x -= speed; | |
gameState.isMoving = true; | |
} | |
if (gameState.keys.right) { | |
gameState.player.x += speed; | |
gameState.isMoving = true; | |
} | |
// Check for collisions with walls | |
if (gameState.player.x < 0) gameState.player.x = 0; | |
if (gameState.player.y < 0) gameState.player.y = 0; | |
if (gameState.player.x > elements.gameContainer.offsetWidth - gameState.player.width) { | |
gameState.player.x = elements.gameContainer.offsetWidth - gameState.player.width; | |
} | |
if (gameState.player.y > elements.gameContainer.offsetHeight - gameState.player.height) { | |
gameState.player.y = elements.gameContainer.offsetHeight - gameState.player.height; | |
} | |
// Check for collisions with obstacles | |
level.obstacles.forEach(obstacle => { | |
if (checkCollision( | |
gameState.player.x, gameState.player.y, gameState.player.width, gameState.player.height, | |
obstacle.x, obstacle.y, obstacle.width, obstacle.height | |
)) { | |
// If obstacle is movable and player is pushing against it | |
if (obstacle.movable && | |
((gameState.keys.right && prevX + gameState.player.width <= obstacle.x) || | |
(gameState.keys.left && prevX >= obstacle.x + obstacle.width) || | |
(gameState.keys.down && prevY + gameState.player.height <= obstacle.y) || | |
(gameState.keys.up && prevY >= obstacle.y + obstacle.height))) { | |
// Move the obstacle slightly in the direction the player is pushing | |
if (gameState.keys.right) obstacle.x += speed * 0.5; | |
if (gameState.keys.left) obstacle.x -= speed * 0.5; | |
if (gameState.keys.down) obstacle.y += speed * 0.5; | |
if (gameState.keys.up) obstacle.y -= speed * 0.5; | |
// Update obstacle position | |
const obstacleElements = document.querySelectorAll('.obstacle'); | |
obstacleElements[level.obstacles.indexOf(obstacle)].style.left = `${obstacle.x}px`; | |
obstacleElements[level.obstacles.indexOf(obstacle)].style.top = `${obstacle.y}px`; | |
} else { | |
// Otherwise, prevent player from moving through the obstacle | |
gameState.player.x = prevX; | |
gameState.player.y = prevY; | |
} | |
} | |
}); | |
// Check for paint spills (slows movement and makes more noise) | |
if (level.paintSpills) { | |
let onPaint = false; | |
level.paintSpills.forEach(paint => { | |
if (checkCollision( | |
gameState.player.x, gameState.player.y, gameState.player.width, gameState.player.height, | |
paint.x, paint.y, paint.width, paint.height | |
)) { | |
onPaint = true; | |
} | |
}); | |
if (onPaint) { | |
speed *= 0.7; // Slow down movement | |
if (gameState.isMoving) { | |
gameState.noiseLevel += gameState.isSneaking ? 0.5 : 1; | |
} | |
} | |
} | |
// Check for creaky tiles (makes more noise) | |
if (level.creakyTiles) { | |
let onCreakyTile = false; | |
level.creakyTiles.forEach(tile => { | |
if (checkCollision( | |
gameState.player.x, gameState.player.y, gameState.player.width, gameState.player.height, | |
tile.x, tile.y, tile.width, tile.height | |
)) { | |
onCreakyTile = true; | |
} | |
}); | |
if (onCreakyTile && gameState.isMoving) { | |
gameState.noiseLevel += gameState.isSneaking ? 1 : 2; | |
} | |
} | |
// Update noise level based on movement | |
if (gameState.isMoving) { | |
gameState.noiseLevel += gameState.isSneaking ? 0.2 : 0.5; | |
} else { | |
gameState.noiseLevel = Math.max(0, gameState.noiseLevel - 0.5); | |
} | |
// Cap noise level | |
gameState.noiseLevel = Math.min(100, Math.max(0, gameState.noiseLevel)); | |
updateNoiseMeter(); | |
// Check for whistle zones | |
if (level.whistleZones) { | |
level.whistleZones.forEach((zone, index) => { | |
// Update zone timers | |
zone.timer = (zone.timer + 1) % zone.interval; | |
if (zone.timer === 0) { | |
zone.active = !zone.active; | |
const zoneElement = document.getElementById(`whistle-zone-${index}`); | |
if (zone.active) { | |
zoneElement.classList.remove('border-transparent'); | |
zoneElement.classList.add('border-red-500', 'bg-red-500', 'bg-opacity-30'); | |
} else { | |
zoneElement.classList.add('border-transparent'); | |
zoneElement.classList.remove('border-red-500', 'bg-red-500', 'bg-opacity-30'); | |
} | |
} | |
// Check if player is in active whistle zone | |
if (zone.active && checkCollision( | |
gameState.player.x, gameState.player.y, gameState.player.width, gameState.player.height, | |
zone.x, zone.y, zone.width, zone.height | |
)) { | |
gameState.noiseLevel = 100; // Immediate detection | |
} | |
}); | |
} | |
// Check for collectibles | |
const collectibles = document.querySelectorAll('.collectible'); | |
collectibles.forEach(collectible => { | |
if (!collectible.classList.contains('hidden') && checkCollision( | |
gameState.player.x, gameState.player.y, gameState.player.width, gameState.player.height, | |
parseInt(collectible.style.left), parseInt(collectible.style.top), 24, 24 | |
)) { | |
collectible.classList.add('hidden'); | |
gameState.collectedCollectibles++; | |
elements.collectiblesCount.textContent = `${gameState.collectedCollectibles}/${level.collectibles}`; | |
// Check if collectible is special (needed to progress) | |
if (collectible.dataset.special === 'true') { | |
if (gameState.currentLevel === 8) { // Canteen level | |
level.enemies[0].distracted = true; | |
level.enemies[0].distractionTimer = 300; // 5 seconds at 60fps | |
} else if (gameState.currentLevel === 9) { // Security guard level | |
level.enemies[0].hasPermission = true; | |
} else if (gameState.currentLevel === 10) { // Final level | |
level.hasPlank = true; | |
} | |
} | |
// Play collect sound | |
playSound('collect'); | |
} | |
}); | |
// Check for exit | |
if (checkCollision( | |
gameState.player.x, gameState.player.y, gameState.player.width, gameState.player.height, | |
level.exit.x, level.exit.y, level.exit.width, level.exit.height | |
)) { | |
// Check if player has required items for certain levels | |
if ((gameState.currentLevel === 9 && !level.enemies[0].hasPermission) || | |
(gameState.currentLevel === 10 && !level.hasPlank)) { | |
// Can't exit yet | |
return; | |
} | |
// Final level has special exit condition | |
if (gameState.currentLevel === 10 && level.hasPlank && !level.bridgeBuilt) { | |
level.bridgeBuilt = true; | |
// Add plank bridge visual | |
const bridge = document.createElement('div'); | |
bridge.className = 'absolute bg-amber-800 h-4'; | |
bridge.style.left = `${level.exit.x + level.exit.width}px`; | |
bridge.style.top = `${level.exit.y + level.exit.height/2 - 2}px`; | |
bridge.style.width = '200px'; | |
bridge.style.transformOrigin = 'left center'; | |
bridge.style.transform = 'rotate(-10deg)'; | |
elements.gameContainer.appendChild(bridge); | |
return; | |
} | |
completeLevel(); | |
return; | |
} | |
// Update enemy positions and check for detection | |
level.enemies.forEach(enemy => { | |
// Handle enemy movement patterns if they exist | |
if (enemy.pattern) { | |
enemy.patternTimer++; | |
if (enemy.patternTimer >= enemy.pattern[enemy.currentPatternStep].duration) { | |
enemy.patternTimer = 0; | |
enemy.currentPatternStep = (enemy.currentPatternStep + 1) % enemy.pattern.length; | |
// Show alert if pattern step has one | |
if (enemy.pattern[enemy.currentPatternStep].alert) { | |
showAlert(enemy.pattern[enemy.currentPatternStep].alert); | |
} | |
} | |
// Only move if current pattern step allows it | |
if (!enemy.pattern[enemy.currentPatternStep].move) { | |
return; | |
} | |
} | |
// Handle distracted enemies (like in canteen level) | |
if (enemy.distracted) { | |
enemy.distractionTimer--; | |
if (enemy.distractionTimer <= 0) { | |
enemy.distracted = false; | |
} | |
return; | |
} | |
// Move enemy along patrol route | |
if (enemy.patrol && enemy.patrol.length > 1) { | |
const currentTarget = enemy.patrol[0]; | |
const nextTarget = enemy.patrol[1]; | |
const dx = nextTarget.x - enemy.x; | |
const dy = nextTarget.y - enemy.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
if (distance < 2) { | |
// Reached target, move to next in patrol | |
enemy.patrol.push(enemy.patrol.shift()); | |
} else { | |
// Move toward target | |
enemy.x += (dx / distance) * 1.5; | |
enemy.y += (dy / distance) * 1.5; | |
} | |
} | |
// Check if player is detected | |
let detected = false; | |
// Check if player is in vision cone (for security guard) | |
if (enemy.visionCone) { | |
detected = isInVisionCone( | |
gameState.player.x + gameState.player.width/2, | |
gameState.player.y + gameState.player.height/2, | |
enemy.x + enemy.width/2, | |
enemy.y + enemy.height/2, | |
enemy.visionCone | |
); | |
// If player has permission slip, they can pass | |
if (detected && enemy.hasPermission) { | |
detected = false; | |
} | |
} | |
// Check if player is in detection range | |
else if (enemy.detectionRange) { | |
const dx = (gameState.player.x + gameState.player.width/2) - (enemy.x + enemy.width/2); | |
const dy = (gameState.player.y + gameState.player.height/2) - (enemy.y + enemy.height/2); | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
detected = distance < enemy.detectionRange && | |
(gameState.noiseLevel > 50 || !gameState.isSneaking); | |
} | |
// Default detection based on noise and line of sight | |
else { | |
// Simple line of sight check | |
const hasLineOfSight = !level.obstacles.some(obstacle => | |
lineIntersectsObstacle( | |
enemy.x + enemy.width/2, enemy.y + enemy.height/2, | |
gameState.player.x + gameState.player.width/2, gameState.player.y + gameState.player.height/2, | |
obstacle | |
) | |
); | |
detected = hasLineOfSight && | |
(gameState.noiseLevel > 70 || | |
(!gameState.isSneaking && gameState.noiseLevel > 30)); | |
} | |
if (detected) { | |
gameOver(); | |
return; | |
} | |
}); | |
updatePlayerPosition(); | |
requestAnimationFrame(gameLoop); | |
} | |
function checkCollision(x1, y1, w1, h1, x2, y2, w2, h2) { | |
return x1 < x2 + w2 && | |
x1 + w1 > x2 && | |
y1 < y2 + h2 && | |
y1 + h1 > y2; | |
} | |
function isInVisionCone(px, py, ex, ey, cone) { | |
// Simple point-in-polygon test for triangle vision cone | |
const n = cone.length; | |
let inside = false; | |
for (let i = 0, j = n - 1; i < n; j = i++) { | |
const xi = cone[i].x + (i === 0 ? ex : 0); | |
const yi = cone[i].y + (i === 0 ? ey : 0); | |
const xj = cone[j].x + (j === 0 ? ex : 0); | |
const yj = cone[j].y + (j === 0 ? ey : 0); | |
const intersect = ((yi > py) !== (yj > py)) && | |
(px < (xj - xi) * (py - yi) / (yj - yi) + xi); | |
if (intersect) inside = !inside; | |
} | |
return inside; | |
} | |
function lineIntersectsObstacle(x1, y1, x2, y2, obstacle) { | |
// Check if line from (x1,y1) to (x2,y2) intersects with the obstacle | |
const left = lineLine(x1, y1, x2, y2, obstacle.x, obstacle.y, obstacle.x, obstacle.y + obstacle.height); | |
const right = lineLine(x1, y1, x2, y2, obstacle.x + obstacle.width, obstacle.y, obstacle.x + obstacle.width, obstacle.y + obstacle.height); | |
const top = lineLine(x1, y1, x2, y2, obstacle.x, obstacle.y, obstacle.x + obstacle.width, obstacle.y); | |
const bottom = lineLine(x1, y1, x2, y2, obstacle.x, obstacle.y + obstacle.height, obstacle.x + obstacle.width, obstacle.y + obstacle.height); | |
return left || right || top || bottom; | |
} | |
function lineLine(x1, y1, x2, y2, x3, y3, x4, y4) { | |
// Calculate the direction of the lines | |
const uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); | |
const uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); | |
// If uA and uB are between 0-1, lines are colliding | |
return uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1; | |
} | |
function handleInteraction() { | |
const level = gameState.levels[gameState.currentLevel - 1]; | |
// Check if near any movable obstacles | |
if (level.obstacles.some(obstacle => obstacle.movable && | |
checkCollision( | |
gameState.player.x - 30, gameState.player.y - 30, | |
gameState.player.width + 60, gameState.player.height + 60, | |
obstacle.x, obstacle.y, obstacle.width, obstacle.height | |
))) { | |
playSound('push'); | |
return; | |
} | |
// Check if near exit with plank (final level) | |
if (gameState.currentLevel === 10 && level.hasPlank && !level.bridgeBuilt && | |
checkCollision( | |
gameState.player.x, gameState.player.y, | |
gameState.player.width, gameState.player.height, | |
level.exit.x - 30, level.exit.y - 30, | |
level.exit.width + 60, level.exit.height + 60 | |
)) { | |
playSound('build'); | |
return; | |
} | |
} | |
function updateNoiseMeter() { | |
elements.noiseMeter.style.width = `${gameState.noiseLevel}%`; | |
if (gameState.noiseLevel > 70) { | |
elements.noiseMeter.classList.remove('bg-yellow-500'); | |
elements.noiseMeter.classList.add('bg-red-500'); | |
} else if (gameState.noiseLevel > 30) { | |
elements.noiseMeter.classList.remove('bg-red-500', 'bg-green-500'); | |
elements.noiseMeter.classList.add('bg-yellow-500'); | |
} else { | |
elements.noiseMeter.classList.remove('bg-yellow-500', 'bg-red-500'); | |
elements.noiseMeter.classList.add('bg-green-500'); | |
} | |
} | |
function showAlert(message) { | |
const alertElement = document.createElement('div'); | |
alertElement.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-black bg-opacity-80 text-white px-6 py-3 rounded-lg z-50 text-center'; | |
alertElement.textContent = message; | |
document.body.appendChild(alertElement); | |
setTimeout(() => { | |
alertElement.remove(); | |
}, 2000); | |
} | |
function startTimer() { | |
stopTimer(); | |
gameState.time = 0; | |
updateTimerDisplay(); | |
gameState.timerInterval = setInterval(() => { | |
gameState.time++; | |
updateTimerDisplay(); | |
}, 1000); | |
} | |
function stopTimer() { | |
if (gameState.timerInterval) { | |
clearInterval(gameState.timerInterval); | |
gameState.timerInterval = null; | |
} | |
} | |
function updateTimerDisplay() { | |
const minutes = Math.floor(gameState.time / 60); | |
const seconds = gameState.time % 60; | |
elements.timer.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
} | |
function pauseGame() { | |
gameState.gameActive = false; | |
stopTimer(); | |
elements.pauseMenu.classList.remove('hidden'); | |
} | |
function resumeGame() { | |
gameState.gameActive = true; | |
startTimer(); | |
elements.pauseMenu.classList.add('hidden'); | |
requestAnimationFrame(gameLoop); | |
} | |
function restartLevel() { | |
elements.pauseMenu.classList.add('hidden'); | |
elements.gameOverModal.classList.add('modal-hidden'); | |
initializeLevel(); | |
} | |
function quitToMenu() { | |
elements.pauseMenu.classList.add('hidden'); | |
elements.gameOverModal.classList.add('modal-hidden'); | |
showMainMenu(); | |
} | |
function completeLevel() { | |
gameState.gameActive = false; | |
stopTimer(); | |
// Calculate score | |
const level = gameState.levels[gameState.currentLevel - 1]; | |
const timeBonus = Math.max(0, 300 - gameState.time) * 10; // Up to 3000 points for speed | |
const collectibleBonus = (gameState.collectedCollectibles / level.collectibles) * 2000; | |
const levelScore = timeBonus + collectibleBonus; | |
gameState.score += levelScore; | |
gameState.totalCollectibles += gameState.collectedCollectibles; | |
// Update complete modal | |
const minutes = Math.floor(gameState.time / 60); | |
const seconds = gameState.time % 60; | |
elements.completeTime.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
elements.completeCollectibles.textContent = `${gameState.collectedCollectibles}/${level.collectibles}`; | |
elements.completeScore.textContent = Math.round(levelScore); | |
// Play success | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=sohamnilkanth7/sohamnilkanrh" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |