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
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;