const createScene = async function () { const canvas = engine.getRenderingCanvas(); const scene = new BABYLON.Scene(engine); // ========================================================= // DADI MA: THE UNTOLD STORY // Babylon.js single-file prototype // ========================================================= scene.clearColor = new BABYLON.Color3(0.015, 0.012, 0.01); scene.fogMode = BABYLON.Scene.FOGMODE_EXP2; scene.fogDensity = 0.035; scene.fogColor = new BABYLON.Color3(0.03, 0.025, 0.025); // --------------------------------------------------------- // Helpers // --------------------------------------------------------- function mat(name, color, emissive = null) { const m = new BABYLON.StandardMaterial(name, scene); m.diffuseColor = color; m.specularColor = new BABYLON.Color3(0.08, 0.08, 0.08); if (emissive) { m.emissiveColor = emissive; } return m; } function box(name, pos, size, material, collidable = true) { const b = BABYLON.MeshBuilder.CreateBox(name, { width: size.x, height: size.y, depth: size.z }, scene); b.position = pos; b.material = material; b.checkCollisions = collidable; b.isPickable = true; return b; } function cylinder(name, pos, diameter, height, material) { const c = BABYLON.MeshBuilder.CreateCylinder(name, { diameter, height, tessellation: 16 }, scene); c.position = pos; c.material = material; return c; } function showMessage(text, seconds = 4) { messageBox.innerHTML = text.replace(/\n/g, "
"); messageBox.style.display = "block"; clearTimeout(messageTimer); messageTimer = setTimeout(() => { messageBox.style.display = "none"; }, seconds * 1000); } function setObjective(text) { objectiveText.textContent = "OBJECTIVE: " + text; gameState.objective = text; } function distance(a, b) { return BABYLON.Vector3.Distance(a, b); } // --------------------------------------------------------- // Materials // --------------------------------------------------------- const floorMat = mat( "Floor", new BABYLON.Color3(0.18, 0.12, 0.08) ); const wallMat = mat( "Walls", new BABYLON.Color3(0.32, 0.23, 0.17) ); const darkWallMat = mat( "DarkWalls", new BABYLON.Color3(0.15, 0.10, 0.08) ); const woodMat = mat( "Wood", new BABYLON.Color3(0.18, 0.08, 0.035) ); const redMat = mat( "Red", new BABYLON.Color3(0.30, 0.025, 0.02) ); const goldMat = mat( "Gold", new BABYLON.Color3(0.62, 0.44, 0.16) ); const keyMat = mat( "Key", new BABYLON.Color3(0.65, 0.65, 0.55) ); const diyaMat = mat( "Diya", new BABYLON.Color3(0.70, 0.38, 0.10), new BABYLON.Color3(0.18, 0.05, 0.01) ); const greenMat = mat( "Flowers", new BABYLON.Color3(0.70, 0.22, 0.06) ); const waterMat = mat( "HolyWater", new BABYLON.Color3(0.15, 0.45, 0.75) ); const whiteMat = mat( "White", new BABYLON.Color3(0.75, 0.75, 0.75) ); // --------------------------------------------------------- // Lighting // --------------------------------------------------------- const hemi = new BABYLON.HemisphericLight( "Hemi", new BABYLON.Vector3(0, 1, 0), scene ); hemi.intensity = 0.25; hemi.diffuse = new BABYLON.Color3(0.35, 0.40, 0.50); hemi.groundColor = new BABYLON.Color3(0.06, 0.03, 0.02); const moon = new BABYLON.DirectionalLight( "Moon", new BABYLON.Vector3(-0.4, -1, 0.25), scene ); moon.intensity = 0.75; moon.diffuse = new BABYLON.Color3(0.45, 0.55, 0.75); // --------------------------------------------------------- // World // --------------------------------------------------------- const ground = box( "Ground", new BABYLON.Vector3(0, -0.2, 0), new BABYLON.Vector3(36, 0.4, 36), floorMat ); // Perimeter box("NorthWall", new BABYLON.Vector3(0, 2.5, 17), new BABYLON.Vector3(34, 5, 0.5), wallMat); box("SouthWall", new BABYLON.Vector3(0, 2.5, -17), new BABYLON.Vector3(34, 5, 0.5), wallMat); box("WestWall", new BABYLON.Vector3(-17, 2.5, 0), new BABYLON.Vector3(0.5, 5, 34), wallMat); box("EastWall", new BABYLON.Vector3(17, 2.5, 0), new BABYLON.Vector3(0.5, 5, 34), wallMat); // Interior architectural pieces box("WestWing", new BABYLON.Vector3(-8, 2.2, 0), new BABYLON.Vector3(0.45, 4.4, 27), wallMat); box("EastWing", new BABYLON.Vector3(8, 2.2, 0), new BABYLON.Vector3(0.45, 4.4, 27), wallMat); box("NorthRoomWall", new BABYLON.Vector3(0, 2.2, -7), new BABYLON.Vector3(16, 4.4, 0.45), wallMat); box("SouthRoomWall", new BABYLON.Vector3(0, 2.2, 7), new BABYLON.Vector3(16, 4.4, 0.45), wallMat); // Courtyard box( "Courtyard", new BABYLON.Vector3(0, 0.04, 0), new BABYLON.Vector3(7, 0.08, 7), darkWallMat, false ); // Rajasthan-inspired pillars for (let i = -14; i <= 14; i += 4) { cylinder( "Pillar", new BABYLON.Vector3(i, 2, -15), 0.55, 4, wallMat ); } // Broken furniture / environment clutter for (let i = 0; i < 18; i++) { const x = -14 + Math.random() * 28; const z = -14 + Math.random() * 28; box( "Furniture_" + i, new BABYLON.Vector3(x, 0.5, z), new BABYLON.Vector3(0.7, 1, 0.7), woodMat, false ); } // Old clock const clockBody = cylinder( "Old Clock", new BABYLON.Vector3(-12, 2, -14), 1, 2, woodMat ); clockBody.rotation.z = Math.PI / 2; // Painting frames for (let i = 0; i < 5; i++) { const frame = box( "PaintingFrame", new BABYLON.Vector3(-7.7, 2.2, -5 + i * 2), new BABYLON.Vector3(0.15, 1.3, 1.8), woodMat, false ); const painting = box( "Painting", new BABYLON.Vector3(-7.58, 2.2, -5 + i * 2), new BABYLON.Vector3(0.05, 1.0, 1.5), i % 2 === 0 ? redMat : darkWallMat, false ); } // Dust-like particles const dust = new BABYLON.ParticleSystem( "Dust", 350, scene ); dust.particleTexture = new BABYLON.Texture( "https://assets.babylonjs.com/textures/flare.png", scene ); dust.emitter = new BABYLON.Vector3(0, 2, 0); dust.minEmitBox = new BABYLON.Vector3(-15, -1, -15); dust.maxEmitBox = new BABYLON.Vector3(15, 7, 15); dust.color1 = new BABYLON.Color4(0.65, 0.55, 0.42, 0.12); dust.color2 = new BABYLON.Color4(0.80, 0.72, 0.60, 0.08); dust.minSize = 0.02; dust.maxSize = 0.08; dust.minLifeTime = 3; dust.maxLifeTime = 7; dust.emitRate = 40; dust.gravity = new BABYLON.Vector3(0, -0.01, 0); dust.start(); // --------------------------------------------------------- // Ritual area // --------------------------------------------------------- const pujaArea = cylinder( "PujaArea", new BABYLON.Vector3(-12, 0.12, 12), 4, 0.25, redMat ); const altar = box( "Altar", new BABYLON.Vector3(-12, 0.8, 12), new BABYLON.Vector3(1.8, 1.2, 1), woodMat ); // --------------------------------------------------------- // Basement Gate // --------------------------------------------------------- const basementGate = box( "BasementGate", new BABYLON.Vector3(12, 1.2, 14.2), new BABYLON.Vector3(3, 2.4, 0.35), woodMat ); // --------------------------------------------------------- // Friend Cage // --------------------------------------------------------- const cageBase = box( "CageBase", new BABYLON.Vector3(12, 0.15, -13), new BABYLON.Vector3(4, 0.3, 4), darkWallMat ); for (let i = 0; i < 10; i++) { const angle = (i / 10) * Math.PI * 2; const x = 12 + Math.cos(angle) * 1.7; const z = -13 + Math.sin(angle) * 1.7; const bar = cylinder( "CageBar", new BABYLON.Vector3(x, 1.5, z), 0.08, 3, whiteMat ); bar.rotation.x = 0; } // --------------------------------------------------------- // Hidden family photograph // --------------------------------------------------------- const photoFrame = box( "FamilyPhoto", new BABYLON.Vector3(-14, 2, -13), new BABYLON.Vector3(0.15, 2, 2.8), woodMat, false ); const photo = box( "Photo", new BABYLON.Vector3(-13.88, 2, -13), new BABYLON.Vector3(0.05, 1.6, 2.4), wallMat, false ); // --------------------------------------------------------- // Player // --------------------------------------------------------- const camera = new BABYLON.UniversalCamera( "SushilCamera", new BABYLON.Vector3(0, 1.7, 13), scene ); camera.attachControl(canvas, true); camera.speed = 0.35; camera.inertia = 0.5; camera.angularSensibility = 3500; camera.minZ = 0.1; camera.applyGravity = true; camera.checkCollisions = true; camera.ellipsoid = new BABYLON.Vector3(0.45, 0.9, 0.45); scene.gravity = new BABYLON.Vector3(0, -0.32, 0); // Flashlight const flashlight = new BABYLON.SpotLight( "Flashlight", camera.position, new BABYLON.Vector3(0, 0, 1), Math.PI / 6, 2, scene ); flashlight.parent = camera; flashlight.position = new BABYLON.Vector3(0, 0, 0.15); flashlight.intensity = 4; flashlight.range = 22; // --------------------------------------------------------- // Game state // --------------------------------------------------------- const gameState = { objective: "Explore the haveli and find clues about your friends.", captures: 0, inventory: {}, ritual: { diya: false, incense: false, flowers: false, water: false }, friendsRescued: false, basementOpen: false, gameOver: false, ending: false, hiding: false, protection: 0, noise: 0, sprinting: false, difficulty: "Normal" }; // --------------------------------------------------------- // UI // --------------------------------------------------------- const ui = document.createElement("div"); ui.style.position = "fixed"; ui.style.left = "0"; ui.style.top = "0"; ui.style.width = "100%"; ui.style.height = "100%"; ui.style.pointerEvents = "none"; ui.style.fontFamily = "Arial, sans-serif"; ui.style.color = "white"; ui.style.zIndex = "9999"; document.body.appendChild(ui); const objectiveText = document.createElement("div"); objectiveText.style.position = "absolute"; objectiveText.style.left = "15px"; objectiveText.style.top = "15px"; objectiveText.style.width = "360px"; objectiveText.style.padding = "12px"; objectiveText.style.background = "rgba(0,0,0,.7)"; objectiveText.style.border = "1px solid #69503d"; objectiveText.style.borderRadius = "8px"; objectiveText.style.fontSize = "14px"; ui.appendChild(objectiveText); const stats = document.createElement("div"); stats.style.position = "absolute"; stats.style.right = "15px"; stats.style.top = "15px"; stats.style.padding = "12px"; stats.style.background = "rgba(0,0,0,.7)"; stats.style.border = "1px solid #69503d"; stats.style.borderRadius = "8px"; stats.style.textAlign = "right"; ui.appendChild(stats); const messageBox = document.createElement("div"); messageBox.style.position = "absolute"; messageBox.style.left = "50%"; messageBox.style.bottom = "12%"; messageBox.style.transform = "translateX(-50%)"; messageBox.style.width = "min(760px,85%)"; messageBox.style.padding = "18px"; messageBox.style.background = "rgba(0,0,0,.88)"; messageBox.style.border = "1px solid #806044"; messageBox.style.borderRadius = "8px"; messageBox.style.textAlign = "center"; messageBox.style.fontSize = "18px"; messageBox.style.display = "none"; ui.appendChild(messageBox); const inventoryPanel = document.createElement("div"); inventoryPanel.style.position = "absolute"; inventoryPanel.style.right = "15px"; inventoryPanel.style.bottom = "15px"; inventoryPanel.style.width = "290px"; inventoryPanel.style.padding = "15px"; inventoryPanel.style.background = "rgba(0,0,0,.9)"; inventoryPanel.style.border = "1px solid #806044"; inventoryPanel.style.borderRadius = "8px"; inventoryPanel.style.display = "none"; ui.appendChild(inventoryPanel); const crosshair = document.createElement("div"); crosshair.textContent = "+"; crosshair.style.position = "absolute"; crosshair.style.left = "50%"; crosshair.style.top = "50%"; crosshair.style.transform = "translate(-50%,-50%)"; crosshair.style.fontSize = "22px"; ui.appendChild(crosshair); const protectionButton = document.createElement("button"); protectionButton.textContent = "READ HANUMAN CHALISA"; protectionButton.style.position = "absolute"; protectionButton.style.left = "50%"; protectionButton.style.bottom = "30px"; protectionButton.style.transform = "translateX(-50%)"; protectionButton.style.pointerEvents = "auto"; protectionButton.style.display = "none"; protectionButton.style.padding = "12px 20px"; protectionButton.style.borderRadius = "8px"; protectionButton.style.border = "1px solid #aa8b50"; protectionButton.style.background = "#17120e"; protectionButton.style.color = "white"; protectionButton.onclick = () => { activateProtection(); }; ui.appendChild(protectionButton); function refreshUI() { objectiveText.textContent = "OBJECTIVE: " + gameState.objective; const invNames = Object.keys(gameState.inventory); inventoryPanel.innerHTML = "INVENTORY

" + (invNames.length ? invNames.map(x => "• " + itemDisplayName(x)).join("
") : "Empty" ); stats.innerHTML = "Stamina: " + Math.round(staminaPercent()) + "%
" + "Captures: " + gameState.captures + "/3
" + "Flashlight: " + (flashlightOn ? "ON" : "OFF") + "
" + (gameState.protection > 0 ? "Protection: " + Math.ceil(gameState.protection) + "s" : ""); protectionButton.style.display = canUseProtection() ? "block" : "none"; } function itemDisplayName(id) { const names = { basement_key: "Basement Key", cage_key: "Cage Key", diya: "Diya", incense: "Incense", flowers: "Flowers", holy_water: "Holy Water", ritual_note: "Ritual Instructions", family_clue: "Family Photograph Clue" }; return names[id] || id; } // --------------------------------------------------------- // Item helpers // --------------------------------------------------------- function makePickup(id, position, material, shape = "box") { let mesh; if (shape === "sphere") { mesh = BABYLON.MeshBuilder.CreateSphere( id, { diameter: 0.5 }, scene ); } else { mesh = BABYLON.MeshBuilder.CreateBox( id, { width: 0.45, height: 0.45, depth: 0.45 }, scene ); } mesh.position = position; mesh.material = material; mesh.isPickable = true; mesh.metadata = { pickup: id }; return mesh; } // --------------------------------------------------------- // Pickups // --------------------------------------------------------- const pickups = []; pickups.push( makePickup( "Basement Key", new BABYLON.Vector3(-13, 0.5, -12), keyMat ) ); pickups.push( makePickup( "Cage Key", new BABYLON.Vector3(13, 0.5, 12), keyMat ) ); pickups.push( makePickup( "Diya", new BABYLON.Vector3(-7, 0.5, 11), diyaMat, "sphere" ) ); pickups.push( makePickup( "Incense", new BABYLON.Vector3(-13, 0.5, 5), woodMat ) ); pickups.push( makePickup( "Flowers", new BABYLON.Vector3(5, 0.5, -11), greenMat, "sphere" ) ); pickups.push( makePickup( "Holy Water", new BABYLON.Vector3(13, 0.5, -5), waterMat, "sphere" ) ); pickups.push( makePickup( "Ritual Instructions", new BABYLON.Vector3(-13, 0.5, 1), whiteMat ) ); pickups.push( makePickup( "Family Clue", new BABYLON.Vector3(13, 0.5, -9), goldMat ) ); // --------------------------------------------------------- // Dadi Ma // --------------------------------------------------------- const dadi = new BABYLON.TransformNode( "DadiMa", scene ); const dadiBody = BABYLON.MeshBuilder.CreateCylinder( "DadiBody", { diameterTop: 1.0, diameterBottom: 1.35, height: 1.7 }, scene ); dadiBody.parent = dadi; dadiBody.position.y = 0.9; dadiBody.material = darkWallMat; const dadiHead = BABYLON.MeshBuilder.CreateSphere( "DadiHead", { diameter: 0.85 }, scene ); dadiHead.parent = dadi; dadiHead.position.y = 2; dadiHead.material = new BABYLON.StandardMaterial( "DadiSkin", scene ); dadiHead.material.diffuseColor = new BABYLON.Color3( 0.42, 0.31, 0.29 ); const eyeMat = new BABYLON.StandardMaterial( "DadiEyes", scene ); eyeMat.emissiveColor = new BABYLON.Color3( 0.8, 0, 0 ); const eye1 = BABYLON.MeshBuilder.CreateSphere( "Eye1", { diameter: 0.08 }, scene ); eye1.parent = dadi; eye1.position = new BABYLON.Vector3( -0.15, 2.08, -0.39 ); eye1.material = eyeMat; const eye2 = eye1.clone("Eye2"); eye2.parent = dadi; eye2.position.x = 0.15; dadi.position = new BABYLON.Vector3( 10, 0, 10 ); // --------------------------------------------------------- // Friends // --------------------------------------------------------- const friends = []; function createFriend(name, pos, color) { const friend = BABYLON.MeshBuilder.CreateCapsule( name, { height: 1.8, radius: 0.35 }, scene ); friend.position = pos; friend.material = mat( name + "Mat", color ); friend.isVisible = false; friend.metadata = { friend: name }; friends.push(friend); } createFriend( "Rahul", new BABYLON.Vector3(10.7, 0.95, -13), new BABYLON.Color3(0.15, 0.30, 0.50) ); createFriend( "Amit", new BABYLON.Vector3(12, 0.95, -13), new BABYLON.Color3(0.45, 0.20, 0.10) ); createFriend( "Maya", new BABYLON.Vector3(13.3, 0.95, -13), new BABYLON.Color3(0.45, 0.20, 0.45) ); // --------------------------------------------------------- // Hiding places // --------------------------------------------------------- const hideSpots = []; function createHideSpot(name, position, size) { const spot = box( name, position, size, woodMat, false ); spot.visibility = 0.01; spot.metadata = { hideSpot: true, name }; hideSpots.push(spot); } createHideSpot( "Hide_Bed_1", new BABYLON.Vector3(-12, 0.3, 10), new BABYLON.Vector3(2.5, 1.0, 4) ); createHideSpot( "Hide_Bed_2", new BABYLON.Vector3(12, 0.3, -10), new BABYLON.Vector3(2.5, 1.0, 4) ); createHideSpot( "Hide_Cupboard_1", new BABYLON.Vector3(-14, 1.1, -7), new BABYLON.Vector3(1.8, 2.2, 1) ); // --------------------------------------------------------- // Interaction handling // --------------------------------------------------------- function nearestInteractable() { let nearest = null; let best = 2.5; [...pickups, basementGate, photoFrame, ...friends] .forEach(obj => { if (!obj || !obj.isEnabled()) return; const d = distance( camera.position, obj.getAbsolutePosition ? obj.getAbsolutePosition() : obj.position ); if (d < best) { best = d; nearest = obj; } }); return nearest; } function interact() { const target = nearestInteractable(); if (!target) { // Check ritual if ( distance( camera.position, new BABYLON.Vector3(-12, 1.7, 12) ) < 4 ) { performRitual(); return; } // Check cage if ( distance( camera.position, new BABYLON.Vector3(12, 1.7, -13) ) < 4 ) { rescueFriends(); return; } // Check hiding for (const spot of hideSpots) { if ( distance( camera.position, spot.position ) < 3 ) { toggleHide(); return; } } // Check gate if ( distance( camera.position, basementGate.position ) < 3 ) { useBasementGate(); } return; } if (target.metadata?.pickup) { collect(target); return; } if (target.metadata?.friend) { rescueFriends(); } } function collect(mesh) { const id = normalizePickup(mesh.metadata.pickup); if (!gameState.inventory[id]) { gameState.inventory[id] = true; } mesh.setEnabled(false); if (id === "basement_key") { setObjective( "Find the Cage Key." ); } if (id === "cage_key") { setObjective( "Reach the basement gate." ); } if (id === "ritual_note") { showMessage( "Ritual instructions found.\nThe family used four sacred materials.", 5 ); setObjective( "Collect the Diya, Incense, Flowers and Holy Water." ); } if (id === "family_clue") { showMessage( "The family photograph has one face torn away.", 4 ); } checkRitualMaterials(); refreshUI(); } function normalizePickup(raw) { const map = { "Basement Key": "basement_key", "Cage Key": "cage_key", "Diya": "diya", "Incense": "incense", "Flowers": "flowers", "Holy Water": "holy_water", "Ritual Instructions": "ritual_note", "Family Clue": "family_clue" }; return map[raw] || raw.toLowerCase(); } function checkRitualMaterials() { const ready = gameState.inventory.diya && gameState.inventory.incense && gameState.inventory.flowers && gameState.inventory.holy_water; if (ready) { gameState.ritual.diya = true; gameState.ritual.incense = true; gameState.ritual.flowers = true; gameState.ritual.water = true; setObjective( "Collect both keys, rescue your friends, then complete the ritual." ); } } function useBasementGate() { if ( gameState.inventory.basement_key && gameState.inventory.cage_key ) { gameState.basementOpen = true; basementGate.setEnabled(false); showMessage( "The basement gate opens.\nYour friends are inside.", 4 ); setObjective( "Enter the basement and rescue Rahul, Amit and Maya." ); } else { showMessage( "You cannot open this gate yet. Your friends are trapped inside. You need the Basement Key and Cage Key to rescue them.", 12 ); } } function rescueFriends() { if (!gameState.basementOpen) { showMessage( "The friends are still behind the locked basement gate.", 3 ); return; } if (!gameState.inventory.cage_key) { showMessage( "The cage is locked. Find the Cage Key.", 3 ); return; } if (!gameState.friendsRescued) { gameState.friendsRescued = true; friends.forEach(f => { f.isVisible = true; }); setObjective( "Complete the final puja in the ritual room." ); showMessage( "Rahul: Sushil! You found us!\nAmit: Jaldi!\nMaya: We still have to finish Dadi Ma's ritual.", 7 ); } } function performRitual() { if ( !gameState.inventory.diya || !gameState.inventory.incense || !gameState.inventory.flowers || !gameState.inventory.holy_water ) { showMessage( "You need the Diya, Incense, Flowers and Holy Water.", 4 ); return; } if (!gameState.friendsRescued) { showMessage( "Rescue Rahul, Amit and Maya before completing the final ritual.", 4 ); return; } if (!gameState.ritual.diya) { showMessage( "The ritual materials are not ready.", 3 ); return; } gameState.ending = true; setObjective( "Performing the final ritual..." ); showMessage( "The diya burns.\nThe haveli becomes silent.\nDadi Ma's spirit begins to receive Mukti.", 8 ); dadiAI.state = "RETURN"; setTimeout(() => { showEnding(); }, 8000); } // --------------------------------------------------------- // Hiding // --------------------------------------------------------- function toggleHide() { if (!gameState.hiding) { gameState.hiding = true; camera.speed = 0; showMessage( "You are hiding...", 2 ); } else { gameState.hiding = false; camera.speed = 0.35; showMessage( "You leave your hiding place.", 2 ); } } // --------------------------------------------------------- // Flashlight // --------------------------------------------------------- let flashlightOn = true; function toggleFlashlight() { flashlightOn = !flashlightOn; flashlight.setEnabled(flashlightOn); refreshUI(); } // --------------------------------------------------------- // Stamina // --------------------------------------------------------- let stamina = 100; function staminaPercent() { return stamina; } // --------------------------------------------------------- // Dadi AI // --------------------------------------------------------- const dadiAI = { state: "PATROL", patrolIndex: 0, investigateTarget: new BABYLON.Vector3(0, 0, 0), searchTime: 0, lostTime: 0 }; const patrolPoints = [ new BABYLON.Vector3(10, 0, 10), new BABYLON.Vector3(10, 0, -10), new BABYLON.Vector3(-10, 0, -10), new BABYLON.Vector3(-10, 0, 10) ]; function updateDadi(dt) { if (gameState.ending || gameState.gameOver) return; if (gameState.hiding) return; const d = distance( dadi.position, camera.position ); // Detection const playerMovingFast = currentSpeed > 0.7; const flashlightVisible = flashlightOn && d < 11; if (playerMovingFast && d < 10) { dadiAI.state = "CHASE"; } if (flashlightVisible && d < 8) { dadiAI.state = "INVESTIGATE"; dadiAI.investigateTarget = camera.position.clone(); } if (gameState.noise > 0.55 && d < 12) { dadiAI.state = "INVESTIGATE"; dadiAI.investigateTarget = camera.position.clone(); } switch (dadiAI.state) { case "PATROL": dadiPatrol(dt); break; case "INVESTIGATE": dadiInvestigate(dt); break; case "SEARCH": dadiSearch(dt); break; case "CHASE": dadiChase(dt); break; case "RETURN": dadiReturn(dt); break; } // Payal warning updatePayal(d); // Capture if ( d < 1.35 && gameState.protection <= 0 ) { caughtByDadi(); } } function moveDadiToward(target, speed, dt) { const targetFlat = new BABYLON.Vector3( target.x, dadi.position.y, target.z ); const dir = targetFlat.subtract(dadi.position); if (dir.length() < 0.1) return; dir.normalize(); dadi.position.addInPlace( dir.scale(speed * dt) ); dadi.rotation.y = Math.atan2(dir.x, dir.z); } function dadiPatrol(dt) { const target = patrolPoints[dadiAI.patrolIndex]; moveDadiToward( target, getDadiSpeed() * 0.45, dt ); if ( distance( dadi.position, target ) < 1 ) { dadiAI.patrolIndex = (dadiAI.patrolIndex + 1) % patrolPoints.length; } } function dadiInvestigate(dt) { moveDadiToward( dadiAI.investigateTarget, getDadiSpeed() * 0.65, dt ); if ( distance( dadi.position, dadiAI.investigateTarget ) < 1 ) { dadiAI.state = "SEARCH"; dadiAI.searchTime = 0; } } function dadiSearch(dt) { dadiAI.searchTime += dt; const center = dadiAI.investigateTarget; const t = dadiAI.searchTime * 1.7; const target = new BABYLON.Vector3( center.x + Math.cos(t) * 2, 0, center.z + Math.sin(t) * 2 ); moveDadiToward( target, getDadiSpeed() * 0.45, dt ); if (dadiAI.searchTime > 5) { dadiAI.state = "RETURN"; } } function dadiChase(dt) { if (gameState.hiding) { dadiAI.state = "SEARCH"; return; } dadiAI.lostTime = 0; moveDadiToward( camera.position, getDadiSpeed(), dt ); if ( distance( dadi.position, camera.position ) > 18 ) { dadiAI.state = "RETURN"; } } function dadiReturn(dt) { const target = patrolPoints[dadiAI.patrolIndex]; moveDadiToward( target, getDadiSpeed() * 0.5, dt ); if ( distance( dadi.position, target ) < 1 ) { dadiAI.state = "PATROL"; } } function getDadiSpeed() { if (gameState.difficulty === "Easy") return 2.1; if (gameState.difficulty === "Hard") return 3.7; return 3.0; } // --------------------------------------------------------- // Payal sound // --------------------------------------------------------- let audioContext = null; let payalCooldown = 0; function getAudioContext() { if (!audioContext) { audioContext = new ( window.AudioContext || window.webkitAudioContext )(); } if (audioContext.state === "suspended") { audioContext.resume(); } return audioContext; } function payalBeep(distanceToDadi) { try { const ctx = getAudioContext(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = "sine"; const close = Math.max( 0, Math.min( 1, 1 - distanceToDadi / 14 ) ); osc.frequency.value = 700 + close * 600; gain.gain.value = 0.015 + close * 0.06; osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop( ctx.currentTime + 0.09 ); } catch (e) { // Audio may be blocked until user interaction. } } function updatePayal(d) { payalCooldown -= engine.getDeltaTime() / 1000; if (d < 14 && payalCooldown <= 0) { payalCooldown = Math.max( 0.18, d / 7 ); payalBeep(d); } } // --------------------------------------------------------- // Capture / death // --------------------------------------------------------- function caughtByDadi() { if (gameState.protection > 0) return; gameState.captures++; showMessage( "Dadi Ma caught Sushil.\nCheckpoint restored.", 3 ); camera.position = new BABYLON.Vector3( 0, 1.7, 13 ); gameState.noise = 0; if (gameState.captures >= 3) { deathSequence(); } } function deathSequence() { gameState.gameOver = true; camera.position = new BABYLON.Vector3( 0, 1.7, 8 ); setTimeout(() => { showMessage( "Sushil is restrained in a chair.\n\nDadi Ma appears in front of him.\n\n\"Today I caught you.\"\n\"Today I caught you.\"", 7 ); }, 500); setTimeout(() => { showFinalDeathScreen(); }, 7500); } function showFinalDeathScreen() { const overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.zIndex = "10000"; overlay.style.background = "#030303"; overlay.style.color = "white"; overlay.style.display = "flex"; overlay.style.flexDirection = "column"; overlay.style.alignItems = "center"; overlay.style.justifyContent = "center"; overlay.style.fontFamily = "Arial"; overlay.innerHTML = `
YOU ARE DEAD
Dadi Ma found you three times.
`; document.body.appendChild(overlay); document.getElementById( "restartDadi" ).onclick = () => { location.reload(); }; document.getElementById( "menuDadi" ).onclick = () => { location.reload(); }; } // --------------------------------------------------------- // Protection // --------------------------------------------------------- function canUseProtection() { return ( gameState.inventory.diya && gameState.inventory.incense && gameState.inventory.flowers && gameState.inventory.holy_water && !gameState.gameOver && !gameState.ending ); } function activateProtection() { if (!canUseProtection()) { showMessage( "The emergency protection becomes available after the required puja progression.", 4 ); return; } gameState.protection = 100; showMessage( "READ HANUMAN CHALISA\n\nDIVINE PROTECTION ACTIVE FOR 100 SECONDS.", 4 ); } // --------------------------------------------------------- // Save / load // --------------------------------------------------------- function saveGame() { const save = { position: { x: camera.position.x, y: camera.position.y, z: camera.position.z }, captures: gameState.captures, inventory: gameState.inventory, ritual: gameState.ritual, friendsRescued: gameState.friendsRescued, basementOpen: gameState.basementOpen, objective: gameState.objective, difficulty: gameState.difficulty }; localStorage.setItem( "DADI_MA_BABYLON_SAVE", JSON.stringify(save) ); showMessage( "GAME SAVED.", 2 ); } function loadGame() { const raw = localStorage.getItem( "DADI_MA_BABYLON_SAVE" ); if (!raw) { showMessage( "NO SAVE FOUND.", 2 ); return; } try { const data = JSON.parse(raw); camera.position = new BABYLON.Vector3( data.position.x, data.position.y, data.position.z ); gameState.captures = data.captures || 0; gameState.inventory = data.inventory || {}; gameState.ritual = data.ritual || gameState.ritual; gameState.friendsRescued = !!data.friendsRescued; gameState.basementOpen = !!data.basementOpen; gameState.difficulty = data.difficulty || "Normal"; setObjective( data.objective || "Continue your investigation." ); if (gameState.basementOpen) { basementGate.setEnabled(false); } if (gameState.friendsRescued) { friends.forEach( f => f.isVisible = true ); } // Restore pickup visibility pickups.forEach(p => { const id = normalizePickup( p.metadata.pickup ); if (gameState.inventory[id]) { p.setEnabled(false); } }); refreshUI(); showMessage( "GAME LOADED.", 2 ); } catch (e) { showMessage( "SAVE DATA IS CORRUPTED.", 3 ); } } // --------------------------------------------------------- // Ending // --------------------------------------------------------- function showEnding() { gameState.ending = true; const overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.zIndex = "10000"; overlay.style.background = "linear-gradient(#090705,#000)"; overlay.style.color = "white"; overlay.style.display = "flex"; overlay.style.flexDirection = "column"; overlay.style.alignItems = "center"; overlay.style.justifyContent = "center"; overlay.style.textAlign = "center"; overlay.style.fontFamily = "Georgia,serif"; overlay.style.padding = "30px"; overlay.innerHTML = `
MUKTI

The haveli becomes silent.
Rahul, Amit and Maya are free.
Dadi Ma's spirit finally receives Mukti.

DIRECTOR
GAME DESIGN
PROGRAMMING
3D ART
ANIMATION
SOUND DESIGN
WRITING
SPECIAL THANKS
`; document.body.appendChild(overlay); document.getElementById( "familyPhoto" ).onclick = () => { overlay.innerHTML = `
OLD FAMILY PHOTOGRAPH
👵
DADI MA
👴
DADU
?

Some family secrets were never buried.

DADI MA WILL RETURN

PART 2

`; document.getElementById( "mainMenuEnd" ).onclick = () => { location.reload(); }; }; } // --------------------------------------------------------- // Pause // --------------------------------------------------------- let paused = false; function togglePause() { paused = !paused; if (paused) { showMessage( "PAUSED\nPress ESC to continue.", 99999 ); } else { messageBox.style.display = "none"; } } // --------------------------------------------------------- // Controls // --------------------------------------------------------- let currentSpeed = 0; scene.onBeforeRenderObservable.add(() => { const delta = scene.getEngine().getDeltaTime() / 1000; if (!gameState.gameOver && !gameState.ending && !paused) { updatePlayer(delta); updateDadi(delta); updateProtection(delta); updateInteractionHints(); refreshUI(); } }); function updatePlayer(dt) { let move = new BABYLON.Vector3( 0, 0, 0 ); const forward = camera.getDirection( BABYLON.Axis.Z ); forward.y = 0; forward.normalize(); const right = camera.getDirection( BABYLON.Axis.X ); right.y = 0; right.normalize(); const w = input.forward; const s = input.backward; const a = input.left; const d = input.right; if (w) move.addInPlace(forward); if (s) move.subtractInPlace(forward); if (d) move.addInPlace(right); if (a) move.subtractInPlace(right); if (move.length() > 0) { move.normalize(); } let running = input.sprint && move.length() > 0 && stamina > 0 && !input.crouch; let speed = input.crouch ? 0.12 : running ? 0.45 : 0.23; currentSpeed = move.length() * speed; if (running) { stamina -= 28 * dt; gameState.sprinting = true; gameState.noise = Math.min( 1, gameState.noise + 1.5 * dt ); } else { stamina += 18 * dt; gameState.sprinting = false; gameState.noise = Math.max( 0, gameState.noise - 0.8 * dt ); } stamina = Math.max( 0, Math.min( 100, stamina ) ); if (input.crouch) { gameState.noise = Math.min( gameState.noise, 0.12 ); } if (move.length() > 0) { camera.position.addInPlace( move.scale(speed) ); } } function updateProtection(dt) { if (gameState.protection > 0) { gameState.protection -= dt; if (gameState.protection <= 0) { gameState.protection = 0; showMessage( "DIVINE PROTECTION HAS ENDED.", 3 ); } } } function updateInteractionHints() { // Kept intentionally subtle; interaction is E. } // Keyboard const input = { forward: false, backward: false, left: false, right: false, sprint: false, crouch: false }; window.addEventListener( "keydown", e => { switch (e.code) { case "KeyW": input.forward = true; break; case "KeyS": input.backward = true; break; case "KeyA": input.left = true; break; case "KeyD": input.right = true; break; case "ShiftLeft": case "ShiftRight": input.sprint = true; break; case "ControlLeft": case "ControlRight": input.crouch = true; break; case "KeyE": interact(); break; case "KeyF": toggleFlashlight(); break; case "KeyI": inventoryPanel.style.display = inventoryPanel.style.display === "none" ? "block" : "none"; break; case "KeyP": saveGame(); break; case "KeyH": activateProtection(); break; case "Escape": togglePause(); break; } } ); window.addEventListener( "keyup", e => { switch (e.code) { case "KeyW": input.forward = false; break; case "KeyS": input.backward = false; break; case "KeyA": input.left = false; break; case "KeyD": input.right = false; break; case "ShiftLeft": case "ShiftRight": input.sprint = false; break; case "ControlLeft": case "ControlRight": input.crouch = false; break; } } ); // Touch support let touchStart = null; canvas.addEventListener("touchstart", e => { if (!e.touches[0]) return; touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY }; }, { passive: true }); canvas.addEventListener("touchmove", e => { if (!touchStart || !e.touches[0]) return; const t = e.touches[0]; const dx = t.clientX - touchStart.x; const dy = t.clientY - touchStart.y; if ( Math.abs(dx) > Math.abs(dy) ) { if (dx > 40) { input.right = true; input.left = false; } else if (dx < -40) { input.left = true; input.right = false; } } else { if (dy < -40) { input.forward = true; input.backward = false; } else if (dy > 40) { input.backward = true; input.forward = false; } } }, { passive: true }); canvas.addEventListener("touchend", () => { input.forward = false; input.backward = false; input.left = false; input.right = false; touchStart = null; }, { passive: true }); // --------------------------------------------------------- // Main menu // --------------------------------------------------------- const menu = document.createElement("div"); menu.style.position = "fixed"; menu.style.inset = "0"; menu.style.zIndex = "10001"; menu.style.background = "radial-gradient(circle,#211711,#020202)"; menu.style.color = "white"; menu.style.display = "flex"; menu.style.alignItems = "center"; menu.style.justifyContent = "center"; menu.style.textAlign = "center"; menu.style.fontFamily = "Arial"; menu.innerHTML = `
DADI MA
THE UNTOLD STORY

Sushil has entered an abandoned Rajasthan-inspired haveli. Rahul, Amit and Maya are trapped. Dadi Ma is waiting.



WASD move · Mouse look · Shift sprint · Ctrl crouch · E interact · F flashlight · I inventory · P save · H protection

`; document.body.appendChild(menu); document.getElementById( "startGameButton" ).onclick = () => { gameState.difficulty = document.getElementById( "difficultySelect" ).value; menu.remove(); canvas.focus(); showMessage( "Sushil arrives at the haveli...\nA distant payal echoes inside.", 5 ); setObjective( "Explore the haveli and find clues about your friends." ); }; document.getElementById( "loadGameButton" ).onclick = () => { menu.remove(); loadGame(); showMessage( "Game loaded.", 2 ); }; // --------------------------------------------------------- // Initial state // --------------------------------------------------------- setObjective( gameState.objective ); refreshUI(); // Basic ambient audio starts after first interaction window.addEventListener( "pointerdown", () => { try { getAudioContext(); } catch (e) {} }, { once: true } ); // Save periodically let autosaveTimer = 0; scene.onBeforeRenderObservable.add(() => { autosaveTimer += scene.getEngine().getDeltaTime() / 1000; if (autosaveTimer >= 30) { autosaveTimer = 0; if ( !gameState.gameOver && !gameState.ending ) { const save = { position: { x: camera.position.x, y: camera.position.y, z: camera.position.z }, captures: gameState.captures, inventory: gameState.inventory, ritual: gameState.ritual, friendsRescued: gameState.friendsRescued, basementOpen: gameState.basementOpen, objective: gameState.objective, difficulty: gameState.difficulty }; localStorage.setItem( "DADI_MA_BABYLON_AUTOSAVE", JSON.stringify(save) ); } } }); // Return scene return scene; }; const scene = await createScene(); return scene;