Загрузка данных


    #define _CRT_SECURE_NO_WARNINGS
    #define NOMINMAX
    #include <Windows.h>
    #include <GL/glew.h>
    #include <GL/freeglut.h>
    #include <cstdio>
    #include <cstdlib>
    #include <cmath>
    #include <ctime>
    #include <vector>
    #include <queue>
    #include <string>
    #include <algorithm>

    /* ═══════════════════════════════════════════════════════
    КОНСТАНТЫ
    ═══════════════════════════════════════════════════════ */
    constexpr int   MAZE_W      = 13;       /* размер лабиринта (нечётное!)    */
    constexpr int   MAZE_H      = 13;
    constexpr float CELL_W      = 3.5f;    /* ширина клетки в мировых единицах */
    constexpr float WALL_H      = 2.8f;    /* высота стены                     */
    constexpr float EYE_H       = 1.4f;    /* высота камеры (рост игрока)      */
    constexpr float PLAYER_R    = 0.28f;   /* радиус коллизии игрока           */
    constexpr float PI_F        = 3.14159265358979f;
    constexpr int   SHADOW_SIZE = 1024;    /* разрешение карты теней           */

    /* Биты стен клетки */
    constexpr uint8_t W_N = 0x01, W_S = 0x02, W_E = 0x04, W_W = 0x08, VIS = 0x10;
    constexpr int     DX[4]       = {  0,  0,  1, -1 };
    constexpr int     DY[4]       = { -1,  1,  0,  0 };
    constexpr uint8_t DIR_W[4]   = { W_N, W_S, W_E, W_W };
    constexpr uint8_t OPP_W[4]   = { W_S, W_N, W_W, W_E };

    /* ═══════════════════════════════════════════════════════
    GLSL — ШЕЙДЕР ГЛУБИНЫ (первый проход: карта теней)
    ═══════════════════════════════════════════════════════ */
    static const char* DEPTH_VERT = R"GLSL(
    #version 120
    attribute vec3 aPos;
    uniform mat4 uLightSpace;
    uniform mat4 uModel;
    void main() {
        gl_Position = uLightSpace * uModel * vec4(aPos, 1.0);
    }
    )GLSL";

    static const char* DEPTH_FRAG = R"GLSL(
    #version 120
    void main() { /* Глубина пишется автоматически */ }
    )GLSL";

    /* ═══════════════════════════════════════════════════════
    GLSL — ОСНОВНОЙ ШЕЙДЕР (второй проход: освещение + тени)
    ═══════════════════════════════════════════════════════ */
    static const char* MAIN_VERT = R"GLSL(
    #version 120
    attribute vec3 aPos;
    attribute vec3 aNormal;
    uniform mat4 uProj;
    uniform mat4 uView;
    uniform mat4 uModel;
    uniform mat4 uLightSpace;
    varying vec3 vFragPos;
    varying vec3 vNormal;
    varying vec4 vFragPosLight;
    void main() {
        vec4 world    = uModel * vec4(aPos, 1.0);
        vFragPos      = world.xyz;
        vNormal       = normalize(mat3(uModel) * aNormal);
        vFragPosLight = uLightSpace * world;
        gl_Position   = uProj * uView * world;
    }
    )GLSL";

    static const char* MAIN_FRAG = R"GLSL(
    #version 120
    varying vec3 vFragPos;
    varying vec3 vNormal;
    varying vec4 vFragPosLight;

    uniform vec3      uColor;
    uniform vec3      uLightDir;      /* нормаль: ОТ фрагмента К источнику    */
    uniform vec3      uSunColor;
    uniform vec3      uTorchPos;      /* позиция факела (камеры игрока)       */
    uniform vec3      uTorchColor;
    uniform float     uTorchBright;  /* яркость с флик-эффектом              */
    uniform vec3      uViewPos;
    uniform sampler2D uShadowMap;
    uniform float     uFogDensity;
    uniform vec3      uFogColor;

    /* PCF: мягкие тени 3x3 */
    float calcShadow() {
        vec3 proj = vFragPosLight.xyz / vFragPosLight.w * 0.5 + 0.5;
        if (proj.z > 1.0) return 0.0;
        float bias   = max(0.012 * (1.0 - dot(vNormal, uLightDir)), 0.004);
        float shadow = 0.0;
        float ts     = 1.0 / float(1024);
        for (int x = -1; x <= 1; x++) {
            for (int y = -1; y <= 1; y++) {
                float d = texture2D(uShadowMap, proj.xy + vec2(float(x), float(y)) * ts).r;
                shadow += (proj.z - bias > d) ? 1.0 : 0.0;
            }
        }
        return shadow / 9.0;
    }

    void main() {
        /* Процедурные текстуры для поверхностей (с увеличенным контрастом!) */
        vec3 texColor = uColor;
        if (vNormal.y > 0.5) { // Пол: средняя шахматка
            float ch = mod(floor(vFragPos.x * 1.0) + floor(vFragPos.z * 1.0), 2.0);
            texColor = mix(uColor, uColor * 0.4, ch);
        } else if (vNormal.y < -0.5) { // Потолок: крупная плита
            float ch = mod(floor(vFragPos.x * 0.5) + floor(vFragPos.z * 0.5), 2.0);
            texColor = mix(uColor, uColor * 0.3, ch);
        } else { // Стены: кирпичи
            if (uColor.b < 0.6) { // Пропускаем игрока (у одежды b=0.9)
                float bx = floor(vFragPos.x * 2.0 + vFragPos.z * 2.0);
                float by = floor(vFragPos.y * 2.0);
                float ch = mod(bx + by, 2.0);
                texColor = mix(uColor, uColor * 0.55, ch);
            }
        }

        /* Фоновое освещение: увеличили с 0.20 до 0.45, чтобы черных зон не было вообще! */
        vec3 color = 0.45 * uSunColor;

        /* Направленный свет с тенями */
        float diff = max(dot(vNormal, uLightDir), 0.0);
        float sh   = calcShadow();
        color += (1.0 - sh * 0.8) * diff * uSunColor * 0.6;

        /* Факел — точечный источник без тени */
        vec3  toT   = uTorchPos - vFragPos;
        float tDist = length(toT);
        /* Еще больше увеличил дальность: коэффициент 0.025 */
        float tAtt  = uTorchBright / (1.0 + 0.08*tDist + 0.025*tDist*tDist);
        float tDiff = max(dot(vNormal, normalize(toT)), 0.0);
        color += tDiff * tAtt * uTorchColor;

        /* Блик от факела (Blinn-Phong) */
        vec3 viewDir = normalize(uViewPos - vFragPos);
        vec3 halfV   = normalize(normalize(toT) + viewDir);
        float spec   = pow(max(dot(vNormal, halfV), 0.0), 32.0);
        color += spec * 0.5 * tAtt * uTorchColor;

        color *= texColor;

        /* Туман (exponential²) */
        float dist      = length(uViewPos - vFragPos);
        float fogFactor = 1.0 - exp(-uFogDensity * uFogDensity * dist * dist);
        color = mix(color, uFogColor, clamp(fogFactor, 0.0, 1.0));

        gl_FragColor = vec4(color, 1.0);
    }
    )GLSL";

    /* ═══════════════════════════════════════════════════════
    МАТЕМАТИКА: Vec3 и Mat4
    ═══════════════════════════════════════════════════════ */
    struct Vec3 {
        float x = 0, y = 0, z = 0;
        Vec3() = default;
        Vec3(float x, float y, float z) : x(x), y(y), z(z) {}
        Vec3  operator+(const Vec3& o) const { return { x+o.x, y+o.y, z+o.z }; }
        Vec3  operator-(const Vec3& o) const { return { x-o.x, y-o.y, z-o.z }; }
        Vec3  operator*(float s)       const { return { x*s,   y*s,   z*s   }; }
        Vec3& operator+=(const Vec3& o) { x+=o.x; y+=o.y; z+=o.z; return *this; }
        float dot  (const Vec3& o) const { return x*o.x + y*o.y + z*o.z; }
        Vec3  cross(const Vec3& o) const {
            return { y*o.z-z*o.y, z*o.x-x*o.z, x*o.y-y*o.x };
        }
        float len()  const { return sqrtf(x*x + y*y + z*z); }
        Vec3  norm() const { float l=len(); return l>0.f ? (*this)*(1.f/l) : *this; }
    };

    /* Матрица 4×4, столбцовый порядок (column-major, как в OpenGL) */
    struct Mat4 {
        float m[16] = {};

        static Mat4 identity() {
            Mat4 r; r.m[0]=r.m[5]=r.m[10]=r.m[15]=1.f; return r;
        }

        static Mat4 perspective(float fovRad, float aspect, float near_, float far_) {
            Mat4 r;
            float f = 1.0f / tanf(fovRad * 0.5f);
            r.m[0]  = f / aspect;
            r.m[5]  = f;
            r.m[10] = (far_ + near_) / (near_ - far_);
            r.m[11] = -1.0f;
            r.m[14] = (2.0f * far_ * near_) / (near_ - far_);
            return r;
        }

        static Mat4 ortho(float l, float r_, float b, float t, float n, float f) {
            Mat4 m;
            m.m[0]  = 2.f/(r_-l);     m.m[5]  = 2.f/(t-b);
            m.m[10] = -2.f/(f-n);
            m.m[12] = -(r_+l)/(r_-l); m.m[13] = -(t+b)/(t-b);
            m.m[14] = -(f+n)/(f-n);   m.m[15] = 1.f;
            return m;
        }

        static Mat4 lookAt(Vec3 eye, Vec3 center, Vec3 up) {
            Vec3 f = (center - eye).norm();
            Vec3 r = f.cross(up).norm();
            Vec3 u = r.cross(f);
            Mat4 m;
            m.m[0]=r.x; m.m[4]=r.y; m.m[8] =r.z;
            m.m[1]=u.x; m.m[5]=u.y; m.m[9] =u.z;
            m.m[2]=-f.x;m.m[6]=-f.y;m.m[10]=-f.z;
            m.m[12]=-r.dot(eye); m.m[13]=-u.dot(eye);
            m.m[14]= f.dot(eye); m.m[15]= 1.f;
            return m;
        }

        static Mat4 translation(Vec3 t) {
            Mat4 m = identity();
            m.m[12] = t.x; m.m[13] = t.y; m.m[14] = t.z;
            return m;
        }

        static Mat4 rotationY(float angleRad) {
            Mat4 m = identity();
            float c = cosf(angleRad), s = sinf(angleRad);
            m.m[0] = c;  m.m[2] = -s;
            m.m[8] = s;  m.m[10] = c;
            return m;
        }

        Mat4 operator*(const Mat4& o) const {
            Mat4 r;
            for (int col=0; col<4; col++)
                for (int row=0; row<4; row++) {
                    r.m[col*4+row] = 0;
                    for (int k=0; k<4; k++)
                        r.m[col*4+row] += m[k*4+row] * o.m[col*4+k];
                }
            return r;
        }
        const float* ptr() const { return m; }
    };

    /* ═══════════════════════════════════════════════════════
    КЛАСС: Shader
    ═══════════════════════════════════════════════════════ */
    class Shader {
    public:
        GLuint prog = 0;

        bool build(const char* vs, const char* fs,
                const char* attr0 = "aPos", const char* attr1 = "aNormal") {
            auto compile = [](GLenum type, const char* src) -> GLuint {
                GLuint s = glCreateShader(type);
                glShaderSource(s, 1, &src, nullptr);
                glCompileShader(s);
                GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
                if (!ok) {
                    char log[512]; glGetShaderInfoLog(s, 512, nullptr, log);
                    fprintf(stderr, "Shader compile error:\n%s\n", log);
                }
                return s;
            };
            GLuint v = compile(GL_VERTEX_SHADER,   vs);
            GLuint f = compile(GL_FRAGMENT_SHADER, fs);
            prog = glCreateProgram();
            glAttachShader(prog, v); glAttachShader(prog, f);
            glBindAttribLocation(prog, 0, attr0);
            if (attr1) glBindAttribLocation(prog, 1, attr1);
            glLinkProgram(prog);
            glDeleteShader(v); glDeleteShader(f);
            GLint ok; glGetProgramiv(prog, GL_LINK_STATUS, &ok);
            if (!ok) {
                char log[512]; glGetProgramInfoLog(prog, 512, nullptr, log);
                fprintf(stderr, "Program link error:\n%s\n", log);
                return false;
            }
            return true;
        }

        void use() const { glUseProgram(prog); }

        void mat4(const char* n, const Mat4& m) const {
            glUniformMatrix4fv(glGetUniformLocation(prog,n),1,GL_FALSE,m.ptr());
        }
        void vec3(const char* n, Vec3 v) const {
            glUniform3f(glGetUniformLocation(prog,n), v.x, v.y, v.z);
        }
        void f1(const char* n, float f) const {
            glUniform1f(glGetUniformLocation(prog,n), f);
        }
        void i1(const char* n, int i) const {
            glUniform1i(glGetUniformLocation(prog,n), i);
        }
    };

    /* ═══════════════════════════════════════════════════════
    КЛАСС: ShadowMap — FBO с текстурой глубины
    ═══════════════════════════════════════════════════════ */
    class ShadowMap {
    public:
        GLuint fbo = 0, tex = 0;

        void init() {
            glGenTextures(1, &tex);
            glBindTexture(GL_TEXTURE_2D, tex);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
                        SHADOW_SIZE, SHADOW_SIZE, 0,
                        GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
            float bc[] = {1,1,1,1};
            glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bc);

            glGenFramebuffers(1, &fbo);
            glBindFramebuffer(GL_FRAMEBUFFER, fbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                                GL_TEXTURE_2D, tex, 0);
            glDrawBuffer(GL_NONE);
            glReadBuffer(GL_NONE);
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
        }

        void beginCapture() {
            glBindFramebuffer(GL_FRAMEBUFFER, fbo);
            glViewport(0, 0, SHADOW_SIZE, SHADOW_SIZE);
            glClear(GL_DEPTH_BUFFER_BIT);
        }

        void endCapture(int winW, int winH) {
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            glViewport(0, 0, winW, winH);
        }
    };

    /* ═══════════════════════════════════════════════════════
    КЛАСС: Maze — генерация лабиринта (Recursive Backtracker)
    ═══════════════════════════════════════════════════════ */
    struct Point { int x=0, y=0; bool operator==(const Point& o) const { return x==o.x&&y==o.y; } };

    class Maze {
    public:
        uint8_t cells[MAZE_H][MAZE_W] = {};

        void generate() {
            for (int y=0;y<MAZE_H;y++)
                for (int x=0;x<MAZE_W;x++)
                    cells[y][x] = W_N|W_S|W_E|W_W;

            std::vector<Point> stack;
            stack.reserve(MAZE_W * MAZE_H);
            cells[0][0] |= VIS;
            stack.push_back({0,0});

            while (!stack.empty()) {
                Point cur = stack.back();
                std::vector<int> dirs;
                for (int d=0;d<4;d++) {
                    int nx=cur.x+DX[d], ny=cur.y+DY[d];
                    if (nx>=0&&nx<MAZE_W&&ny>=0&&ny<MAZE_H&&!(cells[ny][nx]&VIS))
                        dirs.push_back(d);
                }
                if (dirs.empty()) { stack.pop_back(); }
                else {
                    int d  = dirs[rand()%dirs.size()];
                    int nx = cur.x+DX[d], ny = cur.y+DY[d];
                    cells[cur.y][cur.x] &= ~DIR_W[d];
                    cells[ny][nx]       &= ~OPP_W[d];
                    cells[ny][nx]       |=  VIS;
                    stack.push_back({nx,ny});
                }
            }
            for (int y=0;y<MAZE_H;y++)
                for (int x=0;x<MAZE_W;x++)
                    cells[y][x] &= ~VIS;
        }

        bool hasWall(int x, int y, int d) const {
            if (x<0||x>=MAZE_W||y<0||y>=MAZE_H) return true;
            return (cells[y][x] & DIR_W[d]) != 0;
        }
    };

    /* ═══════════════════════════════════════════════════════
    КЛАСС: Camera — камера от первого лица
    ═══════════════════════════════════════════════════════ */
    class Camera {
    public:
        Vec3  pos   = { CELL_W*0.5f, EYE_H, CELL_W*0.5f };
        float yaw   =  0.0f;   /* горизонтальный поворот (градусы) */
        float pitch =  0.0f;   /* вертикальный поворот             */
        float speed =  4.0f;

        Vec3 front() const {
            float y = yaw   * PI_F / 180.0f;
            float p = pitch * PI_F / 180.0f;
            return Vec3{ cosf(p)*sinf(y), sinf(p), cosf(p)*cosf(y) }.norm();
        }

        Vec3 right() const {
            float y = yaw * PI_F / 180.0f;
            return Vec3{ cosf(y), 0.f, -sinf(y) }.norm();
        }

        Mat4 viewMatrix() const {
            Vec3 f = front();
            return Mat4::lookAt(pos, pos+f, {0,1,0});
        }

        void mouseMove(float dx, float dy, float sens = 0.14f) {
            yaw   += dx * sens;
            pitch -= dy * sens;
            if (pitch >  80.f) pitch =  80.f;
            if (pitch < -80.f) pitch = -80.f;
        }

        /* Движение с коллизией стен */
        void move(const Maze& maze, int dir, float dt) {
            Vec3 fwd = front(); fwd.y=0; fwd=fwd.norm();
            Vec3 rgt = right();
            Vec3 d;
            if      (dir==0) d=fwd;
            else if (dir==1) d=fwd*-1.f;
            else if (dir==2) d=rgt;
            else             d=rgt*-1.f;
            Vec3 delta = d * (speed * dt);

            /* Коллизия по X */
            float nx = pos.x + delta.x;
            int cx=(int)(pos.x/CELL_W), cy=(int)(pos.z/CELL_W);
            cx=std::max(0,std::min(MAZE_W-1,cx)); cy=std::max(0,std::min(MAZE_H-1,cy));
            if (delta.x > 0 && maze.hasWall(cx,cy,2)) {
                float wall = (cx+1)*CELL_W;
                if (nx+PLAYER_R > wall) nx = wall-PLAYER_R;
            }
            if (delta.x < 0 && maze.hasWall(cx,cy,3)) {
                float wall = cx*CELL_W;
                if (nx-PLAYER_R < wall) nx = wall+PLAYER_R;
            }
            pos.x = nx;

            /* Коллизия по Z */
            float nz = pos.z + delta.z;
            cx=(int)(pos.x/CELL_W); cy=(int)(pos.z/CELL_W);
            cx=std::max(0,std::min(MAZE_W-1,cx)); cy=std::max(0,std::min(MAZE_H-1,cy));
            if (delta.z > 0 && maze.hasWall(cx,cy,1)) {
                float wall = (cy+1)*CELL_W;
                if (nz+PLAYER_R > wall) nz = wall-PLAYER_R;
            }
            if (delta.z < 0 && maze.hasWall(cx,cy,0)) {
                float wall = cy*CELL_W;
                if (nz-PLAYER_R < wall) nz = wall+PLAYER_R;
            }
            pos.z = nz;
            pos.y = EYE_H;
        }
    };

    /* ═══════════════════════════════════════════════════════
    ВЕРШИНА геометрии: позиция + нормаль
    ═══════════════════════════════════════════════════════ */
    struct Vtx { float x,y,z, nx,ny,nz; };

    static void addQuad(std::vector<Vtx>& v,
                        float x0,float y0,float z0,
                        float x1,float y1,float z1,
                        float x2,float y2,float z2,
                        float x3,float y3,float z3,
                        float nx,float ny,float nz) {
        auto add = [&](float ax,float ay,float az) {
            v.push_back({ax,ay,az, nx,ny,nz});
        };
        add(x0,y0,z0); add(x1,y1,z1); add(x2,y2,z2);
        add(x0,y0,z0); add(x2,y2,z2); add(x3,y3,z3);
    }

    /* ═══════════════════════════════════════════════════════
    КЛАСС: Game — главный контроллер
    ═══════════════════════════════════════════════════════ */
    class Game {
    public:
        static Game& instance() { static Game g; return g; }

        /* ── Инициализация ─────────────────────────────────── */
        void init(int argc, char** argv) {
            glutInit(&argc, argv);
            glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
            glutInitWindowSize(winW, winH);
            glutInitWindowPosition(80, 60);
            glutCreateWindow("3D Labirint — OpenGL Shadow Mapping");

            glewExperimental = GL_TRUE;
            if (glewInit() != GLEW_OK) { fprintf(stderr,"GLEW error\n"); exit(1); }

            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LESS);
            glEnable(GL_CULL_FACE);
            glCullFace(GL_BACK);

            depthShader.build(DEPTH_VERT, DEPTH_FRAG, "aPos", nullptr);
            mainShader .build(MAIN_VERT,  MAIN_FRAG,  "aPos", "aNormal");

            shadowMap.init();

            srand((unsigned)time(nullptr));
            newGame();

            // При старте показываем меню, поэтому мышь пока не захватываем
            // captureMouse();

            glutDisplayFunc (cbDisplay);
            glutReshapeFunc (cbReshape);
            glutKeyboardFunc(cbKey);
            glutKeyboardUpFunc(cbKeyUp);
            glutMotionFunc  (cbMotion);
            glutPassiveMotionFunc(cbMotion);
            glutTimerFunc(16, cbTimer, 0);

            lastTime = glutGet(GLUT_ELAPSED_TIME);
        }

        void run() {
            printf("=== 3D Labirint — Shadow Mapping + Torch ===\n");
            printf("WASD         — dvizhenie\n");
            printf("Mouse        — obzor\n");
            printf("N            — novaya igra\n");
            printf("M            — minikarта\n");
            printf("F            — fakel vkl/otkl\n");
            printf("+/-          — gustota tumana\n");
            printf("ESC          — kursor / vyhod\n");
            glutMainLoop();
        }

    private:
        Game() = default;

        /* ── Члены ─────────────────────────────────────────── */
        Maze      maze;
        Camera    camera;
        Shader    depthShader, mainShader;
        ShadowMap shadowMap;
        Point     exitPt = { MAZE_W-1, MAZE_H-1 };

        GLuint    vbo = 0;
        int       wallOff=0, wallCnt=0;
        int       floorOff=0, floorCnt=0;
        int       playerOff=0,playerCnt=0;
        int       ceilOff=0,  ceilCnt=0;
        int       exitOff=0,  exitCnt=0;

        Mat4      lightSpace;
        int       torchOff=0, torchCnt=0;
        Vec3      lightDir;
        Vec3      fogColor   = { 0.03f, 0.03f, 0.05f };
        float     fogDensity = 0.025f;
        float     torchBright = 1.0f;
        bool      torchOn    = true;

        int   winW=1024, winH=700;
        bool  mouseCaptured = false;
        int   lastMX=0, lastMY=0;
        bool  firstMouse = true;
        bool  showMap    = true;

        float fps=0.f; int frameCount=0, lastTime=0;
        float walkTime = 0.0f; // Переменная для анимации шагов

        /* Клавиши WASD */
        bool keys[4] = {};
        bool won = false;

        enum GameState { MENU, PLAYING };
        GameState gameState = MENU;

        void newGame() {
            maze.generate();
            camera.pos = { CELL_W*0.5f, EYE_H, CELL_W*0.5f };
            camera.yaw = 0.f; camera.pitch = 0.f;
            won = false;
            buildGeometry();
            computeLightMatrix();
        }

        /* ── Построение VBO геометрии ───────────────────────── */
        void buildGeometry() {
            std::vector<Vtx> verts;

            /* --- Стены --- */
            wallOff = 0;
            for (int cy=0; cy<MAZE_H; cy++) {
                for (int cx=0; cx<MAZE_W; cx++) {
                    float x0=cx*CELL_W, x1=(cx+1)*CELL_W;
                    float z0=cy*CELL_W, z1=(cy+1)*CELL_W;
                    if (maze.hasWall(cx,cy,0)) // Север
                        addQuad(verts, x0,0,z0, x1,0,z0, x1,WALL_H,z0, x0,WALL_H,z0, 0,0,1);
                    if (maze.hasWall(cx,cy,1)) // Юг
                        addQuad(verts, x1,0,z1, x0,0,z1, x0,WALL_H,z1, x1,WALL_H,z1, 0,0,-1);
                    if (maze.hasWall(cx,cy,3)) // Запад
                        addQuad(verts, x0,0,z1, x0,0,z0, x0,WALL_H,z0, x0,WALL_H,z1, 1,0,0);
                    if (maze.hasWall(cx,cy,2)) // Восток
                        addQuad(verts, x1,0,z0, x1,0,z1, x1,WALL_H,z1, x1,WALL_H,z0,-1,0,0);
                }
            }
            wallCnt = (int)verts.size() - wallOff;

            /* --- Пол --- */
            floorOff = (int)verts.size();
            addQuad(verts,
                0,0,0, 0,0,MAZE_H*CELL_W,
                MAZE_W*CELL_W,0,MAZE_H*CELL_W, MAZE_W*CELL_W,0,0, 0,1,0);
            floorCnt = (int)verts.size() - floorOff;

            /* --- Потолок --- */
            ceilOff = (int)verts.size();
            addQuad(verts,
                0,WALL_H,MAZE_H*CELL_W, 0,WALL_H,0,
                MAZE_W*CELL_W,WALL_H,0, MAZE_W*CELL_W,WALL_H,MAZE_H*CELL_W, 0,-1,0);
            ceilCnt = (int)verts.size() - ceilOff;

            /* --- Маркер выхода (горизонтальная плита) --- */
            exitOff = (int)verts.size();
            float ex0=exitPt.x*CELL_W+0.1f, ex1=(exitPt.x+1)*CELL_W-0.1f;
            float ez0=exitPt.y*CELL_W+0.1f, ez1=(exitPt.y+1)*CELL_W-0.1f;
            addQuad(verts, ex0,0.01f,ez0, ex1,0.01f,ez0,
                        ex1,0.01f,ez1, ex0,0.01f,ez1, 0,1,0);
            exitCnt = (int)verts.size() - exitOff;

            /* --- Персонаж (игрок) --- */
            playerOff = (int)verts.size();
            auto addBox = [&](float cx, float cy, float cz, float dx, float dy, float dz) {
                float x0=cx-dx, x1=cx+dx, y0=cy-dy, y1=cy+dy, z0=cz-dz, z1=cz+dz;
                addQuad(verts, x0,y0,z0, x1,y0,z0, x1,y1,z0, x0,y1,z0, 0,0,-1); // Back
                addQuad(verts, x1,y0,z1, x0,y0,z1, x0,y1,z1, x1,y1,z1, 0,0, 1); // Front
                addQuad(verts, x0,y0,z1, x0,y0,z0, x0,y1,z0, x0,y1,z1,-1,0, 0); // Left
                addQuad(verts, x1,y0,z0, x1,y0,z1, x1,y1,z1, x1,y1,z0, 1,0, 0); // Right
                addQuad(verts, x0,y1,z0, x1,y1,z0, x1,y1,z1, x0,y1,z1, 0,1, 0); // Top
                addQuad(verts, x0,y0,z1, x1,y0,z1, x1,y0,z0, x0,y0,z0, 0,-1,0); // Bottom
            };

            // Тело (центр локальных координат 0,0,0)
            // Ноги
            addBox(-0.1f, 0.35f, 0.0f, 0.08f, 0.35f, 0.08f); // Левая нога
            addBox( 0.1f, 0.35f, 0.0f, 0.08f, 0.35f, 0.08f); // Правая нога
            // Туловище
            addBox( 0.0f, 0.95f, 0.0f, 0.15f, 0.25f, 0.1f);
            // Левая рука (опущена)
            addBox(-0.25f, 0.9f, 0.0f, 0.05f, 0.2f, 0.05f);
            // Правая рука (вытянута вперед с фонариком)
            // Модель ориентируем так, что вперед (взгляд камеры) - это +Z
            addBox( 0.20f, 1.0f, 0.25f, 0.04f, 0.04f, 0.25f);
            
            playerCnt = (int)verts.size() - playerOff;

            // Фонарик (в правой руке)
            torchOff = (int)verts.size();
            addBox( 0.20f, 1.0f, 0.55f, 0.05f, 0.05f, 0.08f);
            torchCnt = (int)verts.size() - torchOff;

            /* Загрузка в VBO */
            if (!vbo) glGenBuffers(1, &vbo);
            glBindBuffer(GL_ARRAY_BUFFER, vbo);
            glBufferData(GL_ARRAY_BUFFER,
                        verts.size()*sizeof(Vtx),
                        verts.data(), GL_STATIC_DRAW);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
        }

        /* ── Матрица источника света для shadow map ─────────── */
        void computeLightMatrix() {
            float halfW = MAZE_W * CELL_W * 0.5f;
            float halfH = MAZE_H * CELL_W * 0.5f;
            Vec3 lPos   = { halfW + 8.f, 28.f, -6.f };   /* над сценой, с севера */
            Vec3 lTgt   = { halfW, 0.f, halfH };
            lightDir    = (lPos - lTgt).norm();            /* нормаль к свету      */
            Mat4 lView  = Mat4::lookAt(lPos, lTgt, {0,1,0});
            Mat4 lProj  = Mat4::ortho(-40,40,-40,40, 0.5f, 90.f);
            lightSpace  = lProj * lView;
        }

        /* ── Привязка VBO к атрибутам ───────────────────────── */
        void bindVBO(bool posOnly = false) {
            glBindBuffer(GL_ARRAY_BUFFER, vbo);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vtx), (void*)0);
            if (!posOnly) {
                glEnableVertexAttribArray(1);
                glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vtx),
                                    (void*)(3*sizeof(float)));
            } else {
                glDisableVertexAttribArray(1);
            }
        }

        /* ── ПЕРВЫЙ ПРОХОД: генерация карты теней ───────────── */
        void passShadow() {
            shadowMap.beginCapture();
            glEnable(GL_POLYGON_OFFSET_FILL);
            glPolygonOffset(2.f, 4.f);

            depthShader.use();
            depthShader.mat4("uLightSpace", lightSpace);
            depthShader.mat4("uModel", Mat4::identity());
            bindVBO(true);

            glDrawArrays(GL_TRIANGLES, wallOff,  wallCnt);
            glDrawArrays(GL_TRIANGLES, floorOff, floorCnt);
            glDrawArrays(GL_TRIANGLES, ceilOff,  ceilCnt);
            glDrawArrays(GL_TRIANGLES, exitOff,  exitCnt);

            // Тень от игрока и фонарика с анимацией
            float bobY = sinf(walkTime) * 0.05f; // Плавное движение вверх-вниз
            float swayArm = cosf(walkTime) * 0.03f;
            Mat4 pModel = Mat4::translation(Vec3{camera.pos.x, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
            depthShader.mat4("uModel", pModel);
            glDrawArrays(GL_TRIANGLES, playerOff, playerCnt);

            Mat4 torchModel = Mat4::translation(Vec3{camera.pos.x + swayArm, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
            depthShader.mat4("uModel", torchModel);
            glDrawArrays(GL_TRIANGLES, torchOff, torchCnt);

            glDisable(GL_POLYGON_OFFSET_FILL);
            shadowMap.endCapture(winW, winH);
        }

        /* ── ВТОРОЙ ПРОХОД: основная отрисовка ─────────────── */
        void passMain() {
            glClearColor(fogColor.x, fogColor.y, fogColor.z, 1.f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            Mat4 proj = Mat4::perspective(75.f*PI_F/180.f, (float)winW/winH, 0.1f, 80.f);
            Mat4 view = camera.viewMatrix();

            /* Флик факела */
            float t = glutGet(GLUT_ELAPSED_TIME) * 0.001f;
            float flicker = torchOn
                ? (1.0f + 0.08f*sinf(t*7.f) + 0.04f*sinf(t*13.f) + 0.03f*(rand()%10/100.f))
                : 0.0f;
            Vec3 torchPos = camera.pos + Vec3{0, 0.1f, 0};

            mainShader.use();
            mainShader.mat4("uProj",       proj);
            mainShader.mat4("uView",       view);
            mainShader.mat4("uModel",      Mat4::identity());
            mainShader.mat4("uLightSpace", lightSpace);
            mainShader.vec3("uLightDir",   lightDir);
            mainShader.vec3("uSunColor",   {1.0f, 0.95f, 0.85f}); // Увеличили яркость солнца
            mainShader.vec3("uTorchPos",   torchPos);
            mainShader.vec3("uTorchColor", {1.0f, 0.85f, 0.50f}); // Цвет факела ярче
            mainShader.f1 ("uTorchBright", flicker * 4.0f);       // Максимальная яркость света факела усилена
            mainShader.vec3("uViewPos",    camera.pos);
            mainShader.f1 ("uFogDensity",  fogDensity);
            mainShader.vec3("uFogColor",   fogColor);
            mainShader.i1 ("uShadowMap",   0);

            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, shadowMap.tex);
            bindVBO(false);

            /* Пол */
            mainShader.vec3("uColor", {0.65f, 0.60f, 0.55f}); // Сделали намного светлее
            glDrawArrays(GL_TRIANGLES, floorOff, floorCnt);

            /* Потолок */
            mainShader.vec3("uColor", {0.45f, 0.45f, 0.45f}); // Сделали светлее
            glDrawArrays(GL_TRIANGLES, ceilOff, ceilCnt);

            /* Стены */
            mainShader.vec3("uColor", {0.70f, 0.65f, 0.60f}); // И стены светлее
            glDrawArrays(GL_TRIANGLES, wallOff, wallCnt);

            /* Маркер выхода — золотой */
            mainShader.vec3("uColor", {0.90f, 0.72f, 0.10f});
            glDrawArrays(GL_TRIANGLES, exitOff, exitCnt);

            /* Игрок и Фонарик (тело поворачивается за камерой) */
            float bobY = sinf(walkTime) * 0.05f; // Плавное раскачивание вверх-вниз
            float swayArm = cosf(walkTime) * 0.03f; // Горизонтальное покачивание для фонарика
            
            Mat4 pModel = Mat4::translation(Vec3{camera.pos.x, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
            mainShader.mat4("uModel", pModel);
            
            mainShader.vec3("uColor", {0.2f, 0.5f, 0.9f}); // Ярко-синий цвет одежды ("текстура")
            glDrawArrays(GL_TRIANGLES, playerOff, playerCnt);

            Mat4 torchModel = Mat4::translation(Vec3{camera.pos.x + swayArm, bobY, camera.pos.z}) * Mat4::rotationY(camera.yaw * PI_F / 180.0f);
            mainShader.mat4("uModel", torchModel);
            
            mainShader.vec3("uColor", {0.7f, 0.7f, 0.7f}); // Серый корпус фонарика (светлый вместо черного)
            glDrawArrays(GL_TRIANGLES, torchOff, torchCnt);

            glBindTexture(GL_TEXTURE_2D, 0);
            glUseProgram(0);
        }

        /* ── МИНИКАРТА (2D оверлей) ─────────────────────────── */
        void drawMinimap() {
            constexpr float MS = 5.5f;  /* пикселей на клетку               */
            constexpr float MX = 12.f;  /* отступ от края                   */
            float MY_off = winH - MAZE_H*MS - 12.f;

            glDisable(GL_DEPTH_TEST);
            glMatrixMode(GL_PROJECTION);
            glPushMatrix(); glLoadIdentity();
            gluOrtho2D(0, winW, winH, 0);
            glMatrixMode(GL_MODELVIEW);
            glPushMatrix(); glLoadIdentity();

            /* Подложка */
            glColor4f(0,0,0,0.55f);
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            glBegin(GL_QUADS);
            glVertex2f(MX-3,       MY_off-3);
            glVertex2f(MX+MAZE_W*MS+3, MY_off-3);
            glVertex2f(MX+MAZE_W*MS+3, MY_off+MAZE_H*MS+3);
            glVertex2f(MX-3,       MY_off+MAZE_H*MS+3);
            glEnd();
            glDisable(GL_BLEND);

            /* Стены */
            glColor3f(0.75f, 0.72f, 0.68f);
            glLineWidth(1.0f);
            for (int cy=0;cy<MAZE_H;cy++) {
                for (int cx=0;cx<MAZE_W;cx++) {
                    float lx=MX+cx*MS, ly=MY_off+cy*MS;
                    glBegin(GL_LINES);
                    if (maze.hasWall(cx,cy,0)) {
                        glVertex2f(lx,ly); glVertex2f(lx+MS,ly);
                    }
                    if (maze.hasWall(cx,cy,1)) {
                        glVertex2f(lx,ly+MS); glVertex2f(lx+MS,ly+MS);
                    }
                    if (maze.hasWall(cx,cy,3)) {
                        glVertex2f(lx,ly); glVertex2f(lx,ly+MS);
                    }
                    if (maze.hasWall(cx,cy,2)) {
                        glVertex2f(lx+MS,ly); glVertex2f(lx+MS,ly+MS);
                    }
                    glEnd();
                }
            }

            /* Выход */
            glColor3f(0.9f,0.7f,0.1f);
            glBegin(GL_QUADS);
            glVertex2f(MX+exitPt.x*MS+1,   MY_off+exitPt.y*MS+1);
            glVertex2f(MX+(exitPt.x+1)*MS-1,MY_off+exitPt.y*MS+1);
            glVertex2f(MX+(exitPt.x+1)*MS-1,MY_off+(exitPt.y+1)*MS-1);
            glVertex2f(MX+exitPt.x*MS+1,   MY_off+(exitPt.y+1)*MS-1);
            glEnd();

            /* Игрок */
            float px = MX + (camera.pos.x/CELL_W)*MS;
            float py = MY_off + (camera.pos.z/CELL_W)*MS;
            glColor3f(0.3f,0.6f,1.0f);
            glPointSize(5.0f);
            glBegin(GL_POINTS); glVertex2f(px,py); glEnd();

            /* Стрелка-направление */
            Vec3 f = camera.front(); f.y=0; f=f.norm();
            glColor3f(0.8f,0.9f,1.0f);
            glLineWidth(1.5f);
            glBegin(GL_LINES);
            glVertex2f(px, py);
            glVertex2f(px+f.x*MS*0.8f, py+f.z*MS*0.8f);
            glEnd();

            glMatrixMode(GL_PROJECTION); glPopMatrix();
            glMatrixMode(GL_MODELVIEW);  glPopMatrix();
            glEnable(GL_DEPTH_TEST);
            glLineWidth(1.0f);
        }

        /* ── HUD (текстовые подсказки) ──────────────────────── */
        void drawHUD() {
            glDisable(GL_DEPTH_TEST);
            glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
            gluOrtho2D(0, winW, winH, 0);
            glMatrixMode(GL_MODELVIEW);  glPushMatrix(); glLoadIdentity();

            auto txt = [&](float x, float y, const char* s, void* font=GLUT_BITMAP_HELVETICA_12) {
                glRasterPos2f(x, y);
                for (; *s; s++) glutBitmapCharacter(font, *s);
            };

            char buf[128];
            glColor3f(1.f, 1.f, 0.3f);
            snprintf(buf,sizeof(buf),"FPS: %.1f", fps);
            txt(10,20,buf);

            glColor3f(0.75f,0.75f,0.75f);
            snprintf(buf,sizeof(buf),"Kletka: (%d, %d)  Vyhod: (%d, %d)",
                    (int)(camera.pos.x/CELL_W), (int)(camera.pos.z/CELL_W),
                    exitPt.x, exitPt.y);
            txt(10,38,buf);

            glColor3f(0.5f,1.f,0.5f);
            txt(10, (float)winH-10,
                "WASD - dvizhenie | Mouse - obzor | N - novaya igra | M - mapa | F - fakel | +/- - tuman | ESC - vyhod");

            if (won) {
                glColor3f(1.f,0.9f,0.2f);
                txt(winW/2.f-90.f, winH/2.f, "VY NASHOHDITE VIHOD!", GLUT_BITMAP_HELVETICA_18);
                glColor3f(1,1,1);
                txt(winW/2.f-75.f, winH/2.f+30.f, "N - novaya igra");
            }

            if (!torchOn) {
                glColor3f(0.5f,0.5f,0.5f);
                txt(winW-110.f, 20, "[Fakel: OTKL]");
            }

            glMatrixMode(GL_PROJECTION); glPopMatrix();
            glMatrixMode(GL_MODELVIEW);  glPopMatrix();
            glEnable(GL_DEPTH_TEST);
        }

        /* ── Проверка победы ────────────────────────────────── */
        void checkWin() {
            float ex = (exitPt.x+0.5f)*CELL_W;
            float ez = (exitPt.y+0.5f)*CELL_W;
            float dx = camera.pos.x - ex, dz = camera.pos.z - ez;
            if (sqrtf(dx*dx+dz*dz) < CELL_W*0.65f) won = true;
        }

        /* ── ГЛАВНОЕ МЕНЮ (2D) ──────────────────────────────── */
        void drawMenu() {
            glClearColor(0.1f, 0.1f, 0.15f, 1.0f); // Темно-синий фон меню
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            glDisable(GL_DEPTH_TEST);
            glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
            gluOrtho2D(0, winW, winH, 0);
            glMatrixMode(GL_MODELVIEW);  glPushMatrix(); glLoadIdentity();

            auto txt = [&](float x, float y, const char* s, void* font) {
                glRasterPos2f(x, y);
                for (; *s; s++) glutBitmapCharacter(font, *s);
            };

            glColor3f(0.2f, 0.6f, 1.0f);
            txt(winW / 2.0f - 80.0f, winH / 2.0f - 60.0f, "3D LABIRINT", GLUT_BITMAP_HELVETICA_18);

            glColor3f(1.0f, 1.0f, 1.0f);
            txt(winW / 2.0f - 110.0f, winH / 2.0f,      "[ENTER] - Voyti v igru", GLUT_BITMAP_HELVETICA_18);
            txt(winW / 2.0f - 110.0f, winH / 2.0f + 30.0f, "[ESC]   - Vihod", GLUT_BITMAP_HELVETICA_18);

            glMatrixMode(GL_PROJECTION); glPopMatrix();
            glMatrixMode(GL_MODELVIEW);  glPopMatrix();
            glEnable(GL_DEPTH_TEST);
        }

        /* ── Отображение ────────────────────────────────────── */
        void display() {
            if (gameState == MENU) {
                drawMenu();
                glutSwapBuffers();
                return;
            }

            /* Обновление движения */
            int now = glutGet(GLUT_ELAPSED_TIME);
            float dt = std::min((now - lastTime) * 0.001f, 0.05f);
            lastTime = now;

        bool isMoving = false;
        if (!won) {
            for (int d=0;d<4;d++) {
                if(keys[d]) {
                    camera.move(maze,d,dt);
                    isMoving = true;
                }
            }
            checkWin();
        }
        
        if (isMoving) walkTime += dt * 10.0f; // Увеличиваем фазу анимации при ходьбе
        else walkTime = 0.0f; // Сбрасываем в 0, когда стоим

            passShadow();
            passMain();
            if (showMap) drawMinimap();
            drawHUD();

            frameCount++;
            if (now - lastTime + (int)(dt*1000) >= 500) {
                // update FPS every 0.5s from timer
            }

            glutSwapBuffers();
        }

        /* ── Захват / освобождение мыши ────────────────────── */
        void captureMouse() {
            mouseCaptured = true;
            firstMouse    = true;
            glutSetCursor(GLUT_CURSOR_NONE);
        }
        void releaseMouse() {
            mouseCaptured = false;
            glutSetCursor(GLUT_CURSOR_INHERIT);
        }

        /* ── Колбэки ──────────────────────────────────────── */
        void onKey(unsigned char k) {
            if (gameState == MENU) {
                if (k == 13 || k == ' ') { // Enter или Пробел
                    gameState = PLAYING;
                    lastTime = glutGet(GLUT_ELAPSED_TIME); // Сбрасываем таймер
                    captureMouse();
                } else if (k == 27) { // ESC = Выход
                    exit(0);
                }
                glutPostRedisplay();
                return;
            }

            switch (k) {
            case 'w': case 'W': keys[0]=true; break;
            case 's': case 'S': keys[1]=true; break;
            case 'd': case 'D': keys[3]=true; break; // Исправлена инверсия вправо
            case 'a': case 'A': keys[2]=true; break; // Исправлена инверсия влево
            case 'n': case 'N': newGame(); break;
            case 'm': case 'M': showMap = !showMap; break;
            case 'f': case 'F': torchOn = !torchOn; break;
            case '+': fogDensity = std::min(0.2f, fogDensity+0.005f); break;
            case '-': fogDensity = std::max(0.01f,fogDensity-0.005f); break;
            case 27: // ESC во время игры - выход в меню
                releaseMouse();
                gameState = MENU;
                for(int i=0;i<4;i++) keys[i]=false; 
                break;
            }
            glutPostRedisplay();
        }

        void onKeyUp(unsigned char k) {
            switch (k) {
            case 'w': case 'W': keys[0]=false; break;
            case 's': case 'S': keys[1]=false; break;
            case 'd': case 'D': keys[3]=false; break; // Исправлена инверсия вправо
            case 'a': case 'A': keys[2]=false; break; // Исправлена инверсия влево
            }
        }

        void onMouse(int x, int y) {
            if (gameState == MENU || !mouseCaptured) return;
            int cx = winW/2, cy = winH/2;
            if (firstMouse) { firstMouse=false; glutWarpPointer(cx,cy); return; }
            float dx = (float)(x-cx), dy = (float)(y-cy);
            if (fabsf(dx)+fabsf(dy) < 0.5f) return;
            camera.mouseMove(dx, dy);
            glutWarpPointer(cx, cy);
            glutPostRedisplay();
        }

        void onReshape(int w, int h) {
            if (h==0) h=1;
            winW=w; winH=h;
            glViewport(0,0,w,h);
        }

        void onTimer() {
            int now = glutGet(GLUT_ELAPSED_TIME);
            static int fpsPrev=0; static int fpsFrames=0;
            fpsFrames++;
            if (now - fpsPrev >= 500) {
                fps = fpsFrames * 1000.f / (float)(now-fpsPrev);
                fpsFrames=0; fpsPrev=now;
            }
            glutPostRedisplay();
            glutTimerFunc(16, cbTimer, 0);
        }

        /* ── Статические колбэки ────────────────────────────── */
        static void cbDisplay()                           { instance().display(); }
        static void cbReshape(int w,int h)                { instance().onReshape(w,h); }
        static void cbKey(unsigned char k,int,int)        { instance().onKey(k); }
        static void cbKeyUp(unsigned char k,int,int)      { instance().onKeyUp(k); }
        static void cbMotion(int x,int y)                 { instance().onMouse(x,y); }
        static void cbTimer(int)                          { instance().onTimer(); }
    };

    /* ═══════════════════════════════════════════════════════
    ТОЧКА ВХОДА
    ═══════════════════════════════════════════════════════ */
    int main(int argc, char** argv) {
        Game& g = Game::instance();
        g.init(argc, argv);
        g.run();
        return 0;
    }