🎮ArcadeLab

Dadi ma

by SonicLion71
2415 lines62.1 KB
▶ Play
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, "<br>");
        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 =
            "<b>INVENTORY</b><br><br>" +
            (invNames.length
                ? invNames.map(x => "• " + itemDisplayName(x)).join("<br>")
                : "Empty"
            );

        stats.innerHTML =
            "Stamina: " +
            Math.round(staminaPercent()) +
            "%<br>" +
            "Captures: " +
            gameState.captures +
            "/3<br>" +
            "Flashlight: " +
            (flashlightOn ? "ON" : "OFF") +
            "<br>" +
            (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 = `
            <div style="font-size:56px;
                        color:#b10f0f;
                        letter-spacing:5px;">
                YOU ARE DEAD
            </div>

            <div style="margin:25px;
                        font-size:18px;">
                Dadi Ma found you three times.
            </div>

            <button id="restartDadi"
                style="padding:12px 22px;
                       background:#17120e;
                       color:white;
                       border:1px solid #806044;">
                RESTART
            </button>

            <button id="menuDadi"
                style="padding:12px 22px;
                       margin-top:10px;
                       background:#17120e;
                       color:white;
                       border:1px solid #806044;">
                MAIN MENU
            </button>
        `;

        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 = `
            <div style="font-size:52px;
                        color:#d1ad70;
                        letter-spacing:6px;">
                MUKTI
            </div>

            <p style="max-width:700px;
                      font-size:20px;
                      line-height:1.6;">
                The haveli becomes silent.<br>
                Rahul, Amit and Maya are free.<br>
                Dadi Ma's spirit finally receives Mukti.
            </p>

            <div style="margin-top:25px;
                        font-size:16px;">
                DIRECTOR<br>
                GAME DESIGN<br>
                PROGRAMMING<br>
                3D ART<br>
                ANIMATION<br>
                SOUND DESIGN<br>
                WRITING<br>
                SPECIAL THANKS
            </div>

            <button id="familyPhoto"
                style="margin-top:30px;
                       padding:12px 20px;
                       background:#17120e;
                       color:white;
                       border:1px solid #806044;">
                SHOW FAMILY PHOTOGRAPH
            </button>
        `;

        document.body.appendChild(overlay);

        document.getElementById(
            "familyPhoto"
        ).onclick = () => {
            overlay.innerHTML = `
                <div style="
                    width:min(600px,90vw);
                    padding:70px 30px;
                    background:#54463a;
                    border:10px solid #201914;
                    box-shadow:0 0 50px #000;
                ">

                    <div style="
                        font-size:20px;
                        margin-bottom:30px;
                    ">
                        OLD FAMILY PHOTOGRAPH
                    </div>

                    <div style="
                        height:230px;
                        background:
                        linear-gradient(
                            90deg,
                            #685848,
                            #85705b,
                            #41352c
                        );
                        display:flex;
                        align-items:center;
                        justify-content:space-around;
                        filter:sepia(.5);
                    ">

                        <div>
                            👵
                            <br>
                            DADI MA
                        </div>

                        <div>
                            👴
                            <br>
                            DADU
                        </div>

                        <div style="
                            opacity:.15;
                            filter:blur(8px);
                        ">
                            ?
                        </div>

                    </div>

                    <p style="
                        margin-top:28px;
                        font-style:italic;
                        font-size:18px;
                    ">
                        Some family secrets were never buried.
                    </p>

                    <h2 style="
                        color:#b64035;
                        letter-spacing:3px;
                    ">
                        DADI MA WILL RETURN
                    </h2>

                    <h3>
                        PART 2
                    </h3>

                    <button id="mainMenuEnd"
                        style="
                        padding:12px 20px;
                        background:#17120e;
                        color:white;
                        border:1px solid #806044;
                        ">
                        MAIN MENU
                    </button>
                </div>
            `;

            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 = `
        <div style="
            width:min(650px,90vw);
            padding:35px;
            background:rgba(8,6,5,.94);
            border:1px solid #806044;
            border-radius:12px;
            box-shadow:0 0 60px #000;
        ">

            <div style="
                font-size:48px;
                letter-spacing:7px;
                color:#d6b378;
            ">
                DADI MA
            </div>

            <div style="
                letter-spacing:4px;
                color:#a89578;
                margin-top:8px;
            ">
                THE UNTOLD STORY
            </div>

            <p style="
                line-height:1.7;
                color:#c0b5a8;
            ">
                Sushil has entered an abandoned
                Rajasthan-inspired haveli.
                Rahul, Amit and Maya are trapped.
                Dadi Ma is waiting.
            </p>

            <label>
                Difficulty:
                <select id="difficultySelect"
                    style="
                    background:#16110d;
                    color:white;
                    border:1px solid #806044;
                    padding:7px;
                    margin:10px;
                ">
                    <option>Easy</option>
                    <option selected>Normal</option>
                    <option>Hard</option>
                </select>
            </label>

            <br><br>

            <button id="startGameButton"
                style="
                padding:13px 28px;
                color:white;
                background:#19130f;
                border:1px solid #806044;
                ">
                START GAME
            </button>

            <button id="loadGameButton"
                style="
                padding:13px 28px;
                color:white;
                background:#19130f;
                border:1px solid #806044;
                ">
                LOAD GAME
            </button>

            <p style="
                font-size:12px;
                color:#777;
                margin-top:22px;
            ">
                WASD move · Mouse look · Shift sprint ·
                Ctrl crouch · E interact · F flashlight ·
                I inventory · P save · H protection
            </p>

        </div>
    `;

    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;

Game Source: Dadi ma

Creator: SonicLion71

Libraries: none

Complexity: complex (2415 lines, 62.1 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: dadi-ma-soniclion71" to link back to the original. Then publish at arcadelab.ai/publish.