#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_mixer.h>
#include <GL/gl.h>
#ifndef _WIN32
#include <GL/glext.h>
#else
#define GL_BGRA                       0x80E1
#define GL_CLAMP_TO_EDGE              0x812F
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdint.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif

#define WIN_W 960
#define WIN_H 600
#define TEX_SIZE 256
#define CHUNK_SIZE 12
#define GRID_SIZE (CHUNK_SIZE * 2 + 1)
#define MOVE_SPEED 1.6
#define SPRINT_SPEED 3.2
#define SPRINT_BOB_SPEED 12.0
#define SPRINT_BOB_AMT 0.06
#define ROT_SPEED 2.5
#define PLAYER_R 0.2
#define MAX_CHUNKS 512
#define MAX_RAY_STEPS 128
#define PILLAR_HALF 0.15
#define MAX_RENDER_DIST 48.0
#define STATE_TITLE   0
#define STATE_CUTSCENE 1
#define STATE_PLAY    2

typedef struct { int x, y; } ChunkKey;
typedef struct {
    ChunkKey key;
    int cells[GRID_SIZE][GRID_SIZE];
    int secret_lx, secret_ly;
    int secret_side;
    int valid;
} Chunk;

static Chunk chunks[MAX_CHUNKS];
static int chunk_count = 0;

#define MAX_DOORS 256
typedef struct { int wx, wy, face, open, valid; } Door;
static Door doors[MAX_DOORS];
static int door_count = 0;

static uint32_t chunk_seed(int cx, int cy) {
    uint32_t h = (uint32_t)(cx * 73856093) ^ (uint32_t)(cy * 19349669);
    return h ^ 0xDEADBEEF;
}

static uint32_t xorshift(uint32_t *state) {
    uint32_t x = *state;
    x ^= x << 13; x ^= x >> 17; x ^= x << 5;
    return x;
}

static void generate_chunk(Chunk *ch, int cx, int cy) {
    ch->key = (ChunkKey){cx, cy};
    ch->valid = 1;
    uint32_t seed = chunk_seed(cx, cy);
    int G = GRID_SIZE;
    int grid[G][G];
    memset(grid, 0, sizeof(grid));

    for (int i = 0; i < G; i++) {
        grid[0][i] = 1; grid[G-1][i] = 1;
        grid[i][0] = 1; grid[i][G-1] = 1;
    }

    for (int py = 2; py < G - 2; py += 4)
        for (int px = 2; px < G - 2; px += 4)
            if ((xorshift(&seed) % 100) < 70) grid[py][px] = 2;

    int num_dividers = 2 + xorshift(&seed) % 3;
    for (int d = 0; d < num_dividers; d++) {
        int horizontal = xorshift(&seed) % 2;
        if (horizontal) {
            int wy = 2 + xorshift(&seed) % (G - 4);
            int wx_start = 1 + xorshift(&seed) % (G / 3);
            int wx_end = wx_start + 3 + xorshift(&seed) % (G / 2);
            if (wx_end > G - 2) wx_end = G - 2;
            for (int wx = wx_start; wx < wx_end; wx++) grid[wy][wx] = 1;
            int doors = 1 + xorshift(&seed) % 2;
            for (int dd = 0; dd < doors; dd++) {
                int dx = wx_start + xorshift(&seed) % (wx_end - wx_start);
                grid[wy][dx] = 0;
                if (dx + 1 < G) grid[wy][dx + 1] = 0;
            }
        } else {
            int wx = 2 + xorshift(&seed) % (G - 4);
            int wy_start = 1 + xorshift(&seed) % (G / 3);
            int wy_end = wy_start + 3 + xorshift(&seed) % (G / 2);
            if (wy_end > G - 2) wy_end = G - 2;
            for (int wy = wy_start; wy < wy_end; wy++) grid[wy][wx] = 1;
            int doors = 1 + xorshift(&seed) % 2;
            for (int dd = 0; dd < doors; dd++) {
                int dy = wy_start + xorshift(&seed) % (wy_end - wy_start);
                grid[dy][wx] = 0;
                if (dy + 1 < G) grid[dy + 1][wx] = 0;
            }
        }
    }

    uint32_t edge_seed;
    edge_seed = chunk_seed(cx, cy) ^ 0xA11CE;
    for (int i = 0; i < 3; i++) {
        int dx = 2 + (xorshift(&edge_seed) % (G - 4));
        grid[0][dx] = 0;
        if (dx + 1 < G - 1) grid[0][dx + 1] = 0;
    }
    edge_seed = chunk_seed(cx, cy + 1) ^ 0xA11CE;
    for (int i = 0; i < 3; i++) {
        int dx = 2 + (xorshift(&edge_seed) % (G - 4));
        grid[G-1][dx] = 0;
        if (dx + 1 < G - 1) grid[G-1][dx + 1] = 0;
    }
    edge_seed = chunk_seed(cx, cy) ^ 0xBEEF;
    for (int i = 0; i < 3; i++) {
        int dy = 2 + (xorshift(&edge_seed) % (G - 4));
        grid[dy][0] = 0;
        if (dy + 1 < G - 1) grid[dy + 1][0] = 0;
    }
    edge_seed = chunk_seed(cx + 1, cy) ^ 0xBEEF;
    for (int i = 0; i < 3; i++) {
        int dy = 2 + (xorshift(&edge_seed) % (G - 4));
        grid[dy][G-1] = 0;
        if (dy + 1 < G - 1) grid[dy + 1][G-1] = 0;
    }

    /* tiny passable openings - single-cell gaps in walls */
    for (int y = 2; y < G - 2; y++)
        for (int x = 2; x < G - 2; x++)
            if (grid[y][x] == 1 && (xorshift(&seed) % 100) < 5) {
                int empties = (grid[y-1][x]==0)+(grid[y+1][x]==0)+(grid[y][x-1]==0)+(grid[y][x+1]==0);
                if (empties >= 1 && empties <= 2) grid[y][x] = 0;
            }

    memcpy(ch->cells, grid, sizeof(grid));

    ch->secret_lx = -1; ch->secret_ly = -1; ch->secret_side = 0;
    int count = 0;
    for (int y = 1; y < G - 1; y++)
        for (int x = 1; x < G - 1; x++)
            if (grid[y][x] == 1) count++;
    if (count > 0) {
        int pick = xorshift(&seed) % count;
        int n = 0;
        for (int y = 1; y < G - 1; y++)
            for (int x = 1; x < G - 1; x++)
                if (grid[y][x] == 1) {
                    if (n == pick) { ch->secret_lx = x; ch->secret_ly = y; goto found; }
                    n++;
                }
    }
found:
    if (ch->secret_lx >= 0) {
        int sx = ch->secret_lx, sy = ch->secret_ly;
        if (sx > 0 && grid[sy][sx-1] == 0)        ch->secret_side = 0;
        else if (sx < G-1 && grid[sy][sx+1] == 0) ch->secret_side = 1;
        else if (sy > 0 && grid[sy-1][sx] == 0)   ch->secret_side = 2;
        else if (sy < G-1 && grid[sy+1][sx] == 0) ch->secret_side = 3;
        if (door_count < MAX_DOORS && (xorshift(&seed) % 100) < 30) {
            int wx = cx * G + sx, wy = cy * G + sy;
            double dx = wx + 0.5 - 2.5, dy = wy + 0.5 - 2.5;
            if (dx * dx + dy * dy > 64.0) {
                doors[door_count].wx = wx;
                doors[door_count].wy = wy;
                doors[door_count].face = ch->secret_side;
                doors[door_count].open = 0;
                doors[door_count].valid = 1;
                door_count++;
            }
        }
    }
}

static int world_cell(int wx, int wy) {
    int cx = wx / GRID_SIZE;
    int cy = wy / GRID_SIZE;
    if (wx < 0 && wx % GRID_SIZE != 0) cx--;
    if (wy < 0 && wy % GRID_SIZE != 0) cy--;
    int lx = wx - cx * GRID_SIZE;
    int ly = wy - cy * GRID_SIZE;
    if (lx < 0 || lx >= GRID_SIZE || ly < 0 || ly >= GRID_SIZE) return 1;
    for (int i = 0; i < chunk_count; i++)
        if (chunks[i].valid && chunks[i].key.x == cx && chunks[i].key.y == cy)
            return chunks[i].cells[ly][lx];
    if (chunk_count < MAX_CHUNKS) {
        generate_chunk(&chunks[chunk_count], cx, cy);
        return chunks[chunk_count++].cells[ly][lx];
    }
    return 1;
}

static int is_solid(double wx, double wy) {
    int cx = (int)floor(wx), cy = (int)floor(wy);
    int cell = world_cell(cx, cy);
    if (cell == 1) {
        for (int i = 0; i < door_count; i++)
            if (doors[i].valid && doors[i].open && doors[i].wx == cx && doors[i].wy == cy)
                return 0;
        return 1;
    }
    if (cell == 2) {
        double pcx = cx + 0.5, pcy = cy + 0.5;
        if (fabs(wx - pcx) < PILLAR_HALF && fabs(wy - pcy) < PILLAR_HALF)
            return 1;
    }
    return 0;
}

static double px, py, pa, dirx, diry;
static double cam_pitch = 0.0;
static double cam_y = 0.5;
static double bob_t = 0.0;
static int flashlight_on = 0;
static double flash_spring = 0.0, flash_vel = 0.0;

/* === chat system === */
#define MAX_CHAT_MSGS 32
#define CHAT_MSG_DUR 8000
#define CHAT_FADE_DUR 2000
static char chat_msgs[MAX_CHAT_MSGS][512];
static Uint32 chat_msg_times[MAX_CHAT_MSGS];
static int chat_msg_total = 0;
static int chat_open = 0;
static char chat_buf[256];
static int chat_buf_len = 0;
static int chat_mouse_look_saved = 0;

static void chat_add(const char *msg) {
    int idx = chat_msg_total % MAX_CHAT_MSGS;
    strncpy(chat_msgs[idx], msg, 511);
    chat_msgs[idx][511] = 0;
    chat_msg_times[idx] = SDL_GetTicks();
    chat_msg_total++;
}

static void make_zalgo(char *out, int out_len, const char *text) {
    static const unsigned char marks[][2] = {
        {0xCC,0x80},{0xCC,0x81},{0xCC,0x82},{0xCC,0x83},{0xCC,0x84},{0xCC,0x85},{0xCC,0x86},{0xCC,0x87},
        {0xCC,0x88},{0xCC,0x89},{0xCC,0x8A},{0xCC,0x8B},{0xCC,0x8C},{0xCC,0x8D},{0xCC,0x8E},{0xCC,0x8F},
        {0xCC,0x90},{0xCC,0x91},{0xCC,0x92},{0xCC,0x93},{0xCC,0x94},{0xCC,0x95},{0xCC,0x96},{0xCC,0x97},
        {0xCC,0x98},{0xCC,0x99},{0xCC,0x9A},{0xCC,0x9B},{0xCC,0x9C},{0xCC,0x9D},{0xCC,0x9E},{0xCC,0x9F},
        {0xCC,0xA0},{0xCC,0xA1},{0xCC,0xA2},{0xCC,0xA3},{0xCC,0xA4},{0xCC,0xA5},{0xCC,0xA6},{0xCC,0xA7},
        {0xCC,0xA8},{0xCC,0xA9},{0xCC,0xAA},{0xCC,0xAB},{0xCC,0xAC},{0xCC,0xAD},{0xCC,0xAE},{0xCC,0xAF},
        {0xCC,0xB0},{0xCC,0xB1},{0xCC,0xB2},{0xCC,0xB3},{0xCC,0xB4},{0xCC,0xB5},{0xCC,0xB6},{0xCC,0xB7},
        {0xCC,0xB8},{0xCC,0xB9},{0xCC,0xBA},{0xCC,0xBB},{0xCC,0xBC},{0xCC,0xBD},{0xCC,0xBE},{0xCC,0xBF},
        {0xCD,0x80},{0xCD,0x81},{0xCD,0x82},{0xCD,0x83},{0xCD,0x84},{0xCD,0x85},{0xCD,0x86},{0xCD,0x87},
        {0xCD,0x88},{0xCD,0x89},{0xCD,0x8A},{0xCD,0x8B},{0xCD,0x8C},{0xCD,0x8D},{0xCD,0x8E},{0xCD,0x8F},
        {0xCD,0x90},{0xCD,0x91},{0xCD,0x92},{0xCD,0x93},{0xCD,0x94},{0xCD,0x95},{0xCD,0x96},{0xCD,0x97},
        {0xCD,0x98},{0xCD,0x99},{0xCD,0x9A},{0xCD,0x9B},{0xCD,0x9C},{0xCD,0x9D},{0xCD,0x9E},{0xCD,0x9F},
        {0xCD,0xA0},{0xCD,0xA1},{0xCD,0xA2},{0xCD,0xA3},{0xCD,0xA4},{0xCD,0xA5},{0xCD,0xA6},{0xCD,0xA7},
        {0xCD,0xA8},{0xCD,0xA9},{0xCD,0xAA},{0xCD,0xAB},{0xCD,0xAC},{0xCD,0xAD},{0xCD,0xAE},{0xCD,0xAF},
    };
    int nmarks = sizeof(marks)/sizeof(marks[0]);
    int pos = 0;
    for (const char *p = text; *p && pos < out_len - 6; p++) {
        out[pos++] = *p;
        int add = 1 + (rand() % 4);
        for (int i = 0; i < add && pos < out_len - 3; i++) {
            int mi = rand() % nmarks;
            out[pos++] = (char)marks[mi][0];
            out[pos++] = (char)marks[mi][1];
        }
    }
    out[pos] = 0;
}

static void player_init(void) {
    px = 2.5; py = 2.5; pa = 0;
    dirx = cos(pa); diry = sin(pa);
}

static void player_rotate(double a) {
    pa += a;
    dirx = cos(pa); diry = sin(pa);
}

static int player_collides(double nx, double ny) {
    double r = PLAYER_R;
    int x0 = (int)floor(nx - r), x1 = (int)floor(nx + r);
    int y0 = (int)floor(ny - r), y1 = (int)floor(ny + r);
    for (int cy = y0; cy <= y1; cy++) {
        for (int cx = x0; cx <= x1; cx++) {
            int cell = world_cell(cx, cy);
            if (cell == 1) {
                int blocked = 1;
                for (int i = 0; i < door_count; i++)
                    if (doors[i].valid && doors[i].open && doors[i].wx == cx && doors[i].wy == cy)
                        { blocked = 0; break; }
                if (blocked) return 1;
            }
            if (cell == 2) {
                double pcx = cx + 0.5, pcy = cy + 0.5;
                if (nx + r > pcx - PILLAR_HALF && nx - r < pcx + PILLAR_HALF &&
                    ny + r > pcy - PILLAR_HALF && ny - r < pcy + PILLAR_HALF)
                    return 1;
            }
        }
    }
    return 0;
}

static void player_move(double fwd, double str) {
    double nx = px + dirx * fwd - diry * str;
    double ny = py + diry * fwd + dirx * str;
    if (!player_collides(nx, py)) px = nx;
    if (!player_collides(px, ny)) py = ny;
}

/* ===== GL globals ===== */
static SDL_Window *win;
static GLuint tex_wall, tex_secret, tex_floor_id, tex_ceil, tex_bg;
static GLuint tex_door_open, tex_door_closed;

static GLuint load_gl_tex(const char *path) {
    SDL_Surface *surf = IMG_Load(path);
    if (!surf) { fprintf(stderr, "Failed: %s (%s)\n", path, IMG_GetError()); return 0; }
    SDL_Surface *c = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
    SDL_FreeSurface(surf);
    if (!c) return 0;
    GLuint id;
    glGenTextures(1, &id);
    glBindTexture(GL_TEXTURE_2D, id);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, c->w, c->h, 0, GL_BGRA, GL_UNSIGNED_BYTE, c->pixels);
    SDL_FreeSurface(c);
    return id;
}

static GLuint surf_to_gl(SDL_Surface *surf) {
    SDL_Surface *c = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
    if (!c) return 0;
    GLuint id;
    glGenTextures(1, &id);
    glBindTexture(GL_TEXTURE_2D, id);
    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_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, c->w, c->h, 0, GL_BGRA, GL_UNSIGNED_BYTE, c->pixels);
    SDL_FreeSurface(c);
    return id;
}

static void gl_perspective(double fovY, double aspect, double zNear, double zFar) {
    double f = 1.0 / tan(fovY * M_PI / 360.0);
    double nf = 1.0 / (zNear - zFar);
    double m[16] = {0};
    m[0] = f / aspect; m[5] = f;
    m[10] = (zFar + zNear) * nf; m[11] = -1.0;
    m[14] = 2.0 * zFar * zNear * nf;
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixd(m);
}

static void gl_ortho2d(double l, double r, double b, double t) {
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(l, r, b, t, -1, 1);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

static float cell_light_level(int wx, int wy) {
    double ddx = wx + 0.5 - 2.5, ddy = wy + 0.5 - 2.5;
    if (ddx * ddx + ddy * ddy < 25.0) return 1.0f;
    int bx = wx / 4, by = wy / 4;
    float dark = 0.0f;
    for (int dy = -1; dy <= 1; dy++)
        for (int dx = -1; dx <= 1; dx++) {
            uint32_t h = (uint32_t)((bx+dx) * 73856093) ^ (uint32_t)((by+dy) * 19349669) ^ 0xCAFE;
            if ((h % 100) >= 12) continue;
            double cx = (bx + dx) * 4.0 + 2.0;
            double cy = (by + dy) * 4.0 + 2.0;
            double dist = sqrt((wx + 0.5 - cx) * (wx + 0.5 - cx) + (wy + 0.5 - cy) * (wy + 0.5 - cy));
            double c = 1.0 - dist / 3.5;
            if (c > dark) dark = (float)c;
        }
    if (dark <= 0.0f) return 1.0f;
    float lit = 1.0f - dark;
    if (flash_spring > 0.5f) {
        double fdx = wx + 0.5 - px, fdy = wy + 0.5 - py;
        double fdist = sqrt(fdx * fdx + fdy * fdy);
        if (fdist < 6.0 && fdist > 0.01) {
            double dot = (cos(pa) * fdx + sin(pa) * fdy) / fdist;
            if (dot > 0.7) {
                float atten = (float)(1.0 - fdist / 6.0);
                float beam = 0.5f * atten * (float)((flash_spring - 0.5f) * 2.0f);
                lit = lit + (1.0f - lit) * beam;
            }
        }
    }
    if (lit < 0.08f) lit = 0.08f;
    return lit;
}

static int is_dark_cell(int wx, int wy) {
    return cell_light_level(wx, wy) < 0.5f;
}

static int is_secret_face(int wx, int wy, int face) {
    for (int i = 0; i < chunk_count; i++) {
        if (!chunks[i].valid) continue;
        Chunk *ch = &chunks[i];
        int wx0 = ch->key.x * GRID_SIZE;
        int wy0 = ch->key.y * GRID_SIZE;
        if (wx == wx0 + ch->secret_lx && wy == wy0 + ch->secret_ly) {
            if (face == ch->secret_side) return 1;
        }
    }
    return 0;
}

static const char *ascii_art[] = {
    "   __________  __  _______  __    _______  ___  __",
    "  / ____/ __ \\/  |/  / __ \\/ /   / ____/ |/ / |/ /",
    " / /   / / / / /|_/ / /_/ / /   / __/  |   /|   / ",
    "/ /___/ /_/ / /  / / ____/ /___/ /___ /   |/   |  ",
    "\\____/\\____/_/  /_/_/   /_____/_____//_/|_/_/|_|  ",
    "                                                   ",
    "           R A Y C A S T E R"
};
#define ASCII_LINES (sizeof(ascii_art) / sizeof(ascii_art[0]))

static void render_3d(void) {
    int out_w, out_h;
    SDL_GL_GetDrawableSize(win, &out_w, &out_h);
    glViewport(0, 0, out_w, out_h);

    glClearColor(0, 0, 0, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

    double aspect = (double)out_w / out_h;
    gl_perspective(60.0, aspect, 0.05, 100.0);

    /* view matrix */
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    float pitch_deg = (float)(-cam_pitch * 180.0 / M_PI);
    float yaw_deg = (float)(pa * 180.0 / M_PI + 90.0);
    if (bob_t > 0.01)
        yaw_deg += (float)(sin(bob_t) * SPRINT_BOB_AMT * 180.0 / M_PI);
    glRotatef(pitch_deg, 1, 0, 0);
    glRotatef(yaw_deg, 0, 1, 0);
    glTranslatef((float)(-px), (float)(-cam_y), (float)(-py));

    /* lighting */
    glEnable(GL_LIGHTING);
    glEnable(GL_COLOR_MATERIAL);
    glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT);
    {
        float white[] = {1, 1, 1, 1};
        glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, white);
    }
    {
        float global_ambient[] = {0.95f, 0.95f, 0.95f, 1.0f};
        glLightModelfv(GL_LIGHT_MODEL_AMBIENT, global_ambient);
    }
    if (flash_spring > 0.01f) {
        float light_ambient[] = {0, 0, 0, 1};
        float intensity = flash_spring;
        float light_diffuse[] = {0.5f * intensity, 0.47f * intensity, 0.38f * intensity, 1};
        float light_position[] = {(float)px, (float)cam_y, (float)py, 1.0f};
        float light_direction[] = {
            (float)(cos(pa) * cos(cam_pitch)),
            (float)sin(cam_pitch),
            (float)(sin(pa) * cos(cam_pitch))
        };
        glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
        glLightfv(GL_LIGHT0, GL_POSITION, light_position);
        glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, light_direction);
        glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, 25.0f);
        glLightf(GL_LIGHT0, GL_SPOT_EXPONENT, 32.0f);
        glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 1.0f);
        glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 0.05f);
        glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.0f);
        glEnable(GL_LIGHT0);
    } else {
        glDisable(GL_LIGHT0);
    }

    /* fog */
    glEnable(GL_FOG);
    glFogi(GL_FOG_MODE, GL_LINEAR);
    glFogf(GL_FOG_START, 2.0f);
    glFogf(GL_FOG_END, (float)MAX_RENDER_DIST);
    float fogc[4] = {0, 0, 0, 1};
    glFogfv(GL_FOG_COLOR, fogc);

    /* floor per-cell with per-vertex lighting (Gouraud smooth) */
    glBindTexture(GL_TEXTURE_2D, tex_floor_id);
    glNormal3f(0, 1, 0);
    int pcx_i = (int)floor(px), pcy_i = (int)floor(py);
    int rad = (int)MAX_RENDER_DIST + 1;
    glShadeModel(GL_SMOOTH);
    glBegin(GL_QUADS);
    for (int wy = pcy_i - rad; wy <= pcy_i + rad; wy++)
        for (int wx = pcx_i - rad; wx <= pcx_i + rad; wx++) {
            double dx = wx + 0.5 - px, dy = wy + 0.5 - py;
            if (dx * dx + dy * dy > MAX_RENDER_DIST * MAX_RENDER_DIST) continue;
            float l00 = cell_light_level(wx, wy);
            float l10 = cell_light_level(wx + 1, wy);
            float l01 = cell_light_level(wx, wy + 1);
            float l11 = cell_light_level(wx + 1, wy + 1);
            glColor4f(l00, l00, l00, 1);
            glTexCoord2f(wx * 4.0f, wy * 4.0f); glVertex3f(wx, -0.001f, wy);
            glColor4f(l10, l10, l10, 1);
            glTexCoord2f((wx + 1) * 4.0f, wy * 4.0f); glVertex3f(wx + 1, -0.001f, wy);
            glColor4f(l11, l11, l11, 1);
            glTexCoord2f((wx + 1) * 4.0f, (wy + 1) * 4.0f); glVertex3f(wx + 1, -0.001f, wy + 1);
            glColor4f(l01, l01, l01, 1);
            glTexCoord2f(wx * 4.0f, (wy + 1) * 4.0f); glVertex3f(wx, -0.001f, wy + 1);
        }
    glEnd();

    /* ceiling per-cell with per-vertex lighting (Gouraud smooth) */
    glBindTexture(GL_TEXTURE_2D, tex_ceil);
    glNormal3f(0, -1, 0);
    glBegin(GL_QUADS);
    for (int wy = pcy_i - rad; wy <= pcy_i + rad; wy++)
        for (int wx = pcx_i - rad; wx <= pcx_i + rad; wx++) {
            double dx = wx + 0.5 - px, dy = wy + 0.5 - py;
            if (dx * dx + dy * dy > MAX_RENDER_DIST * MAX_RENDER_DIST) continue;
            float l00 = cell_light_level(wx, wy);
            float l10 = cell_light_level(wx + 1, wy);
            float l01 = cell_light_level(wx, wy + 1);
            float l11 = cell_light_level(wx + 1, wy + 1);
            glColor4f(l00, l00, l00, 1);
            glTexCoord2f(wx * 2.0f, wy * 2.0f); glVertex3f(wx, 1.001f, wy);
            glColor4f(l10, l10, l10, 1);
            glTexCoord2f((wx + 1) * 2.0f, wy * 2.0f); glVertex3f(wx + 1, 1.001f, wy);
            glColor4f(l11, l11, l11, 1);
            glTexCoord2f((wx + 1) * 2.0f, (wy + 1) * 2.0f); glVertex3f(wx + 1, 1.001f, wy + 1);
            glColor4f(l01, l01, l01, 1);
            glTexCoord2f(wx * 2.0f, (wy + 1) * 2.0f); glVertex3f(wx, 1.001f, wy + 1);
        }
    glEnd();

    glColor4f(1, 1, 1, 1);

    /* walls - batch normal + secret separately */

    /* pass 1: normal walls + pillars */
    glBindTexture(GL_TEXTURE_2D, tex_wall);
    glBegin(GL_QUADS);
    for (int wy = pcy_i - rad; wy <= pcy_i + rad; wy++) {
        for (int wx = pcx_i - rad; wx <= pcx_i + rad; wx++) {
            double dx = wx + 0.5 - px;
            double dy = wy + 0.5 - py;
            if (dx * dx + dy * dy > MAX_RENDER_DIST * MAX_RENDER_DIST) continue;
            int cell = world_cell(wx, wy);

            if (cell == 1) {
                if (px < wx && !is_secret_face(wx, wy, 0)) {
                    float l_a = cell_light_level(wx - 1, wy);
                    float l_b = cell_light_level(wx - 1, wy + 1);
                    glNormal3f(-1, 0, 0);
                    glTexCoord2d(0, 0); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx, 0, wy);
                    glTexCoord2d(1, 0); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx, 0, wy + 1);
                    glTexCoord2d(1, 1); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx, 1, wy + 1);
                    glTexCoord2d(0, 1); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx, 1, wy);
                }
                if (px > wx + 1 && !is_secret_face(wx, wy, 1)) {
                    float l_a = cell_light_level(wx + 1, wy + 1);
                    float l_b = cell_light_level(wx + 1, wy);
                    glNormal3f(1, 0, 0);
                    glTexCoord2d(0, 0); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx+1, 0, wy+1);
                    glTexCoord2d(1, 0); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx+1, 0, wy);
                    glTexCoord2d(1, 1); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx+1, 1, wy);
                    glTexCoord2d(0, 1); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx+1, 1, wy+1);
                }
                if (py < wy && !is_secret_face(wx, wy, 2)) {
                    float l_a = cell_light_level(wx + 1, wy - 1);
                    float l_b = cell_light_level(wx, wy - 1);
                    glNormal3f(0, 0, -1);
                    glTexCoord2d(0, 0); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx+1, 0, wy);
                    glTexCoord2d(1, 0); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx, 0, wy);
                    glTexCoord2d(1, 1); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx, 1, wy);
                    glTexCoord2d(0, 1); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx+1, 1, wy);
                }
                if (py > wy + 1 && !is_secret_face(wx, wy, 3)) {
                    float l_a = cell_light_level(wx, wy + 1);
                    float l_b = cell_light_level(wx + 1, wy + 1);
                    glNormal3f(0, 0, 1);
                    glTexCoord2d(0, 0); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx, 0, wy+1);
                    glTexCoord2d(1, 0); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx+1, 0, wy+1);
                    glTexCoord2d(1, 1); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx+1, 1, wy+1);
                    glTexCoord2d(0, 1); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx, 1, wy+1);
                }
            } else if (cell == 2) {
                double cx = wx + 0.5, cy = wy + 0.5;
                double b0 = cx - PILLAR_HALF, b1 = cx + PILLAR_HALF;
                double c0 = cy - PILLAR_HALF, c1 = cy + PILLAR_HALF;
                double pw = PILLAR_HALF * 2.0;
                if (px < b0) {
                    float l = cell_light_level(wx - 1, wy);
                    float l_bot = l, l_top = l * 0.45f;
                    glNormal3f(-1, 0, 0);
                    glTexCoord2d(0, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b0, 0, c0);
                    glTexCoord2d(pw, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b0, 0, c1);
                    glTexCoord2d(pw, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b0, 1, c1);
                    glTexCoord2d(0, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b0, 1, c0);
                }
                if (px > b1) {
                    float l = cell_light_level(wx + 1, wy);
                    float l_bot = l, l_top = l * 0.45f;
                    glNormal3f(1, 0, 0);
                    glTexCoord2d(0, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b1, 0, c1);
                    glTexCoord2d(pw, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b1, 0, c0);
                    glTexCoord2d(pw, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b1, 1, c0);
                    glTexCoord2d(0, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b1, 1, c1);
                }
                if (py < c0) {
                    float l = cell_light_level(wx, wy - 1);
                    float l_bot = l, l_top = l * 0.45f;
                    glNormal3f(0, 0, -1);
                    glTexCoord2d(0, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b1, 0, c0);
                    glTexCoord2d(pw, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b0, 0, c0);
                    glTexCoord2d(pw, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b0, 1, c0);
                    glTexCoord2d(0, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b1, 1, c0);
                }
                if (py > c1) {
                    float l = cell_light_level(wx, wy + 1);
                    float l_bot = l, l_top = l * 0.45f;
                    glNormal3f(0, 0, 1);
                    glTexCoord2d(0, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b0, 0, c1);
                    glTexCoord2d(pw, 0); glColor4f(l_bot, l_bot, l_bot, 1); glVertex3d(b1, 0, c1);
                    glTexCoord2d(pw, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b1, 1, c1);
                    glTexCoord2d(0, 1); glColor4f(l_top, l_top, l_top, 1); glVertex3d(b0, 1, c1);
                }
            }
        }
    }
    glEnd();

    /* pass 2: secret walls + doors */
    for (int wy = pcy_i - rad; wy <= pcy_i + rad; wy++) {
        for (int wx = pcx_i - rad; wx <= pcx_i + rad; wx++) {
            double dx = wx + 0.5 - px;
            double dy = wy + 0.5 - py;
            if (dx * dx + dy * dy > MAX_RENDER_DIST * MAX_RENDER_DIST) continue;
            int cell = world_cell(wx, wy);
            if (cell != 1) continue;
            if (!is_secret_face(wx, wy, 0) && !is_secret_face(wx, wy, 1) &&
                !is_secret_face(wx, wy, 2) && !is_secret_face(wx, wy, 3)) continue;

            int door_idx = -1;
            for (int i = 0; i < door_count; i++)
                if (doors[i].valid && doors[i].wx == wx && doors[i].wy == wy)
                    { door_idx = i; break; }

            GLuint tex = door_idx >= 0 ? (doors[door_idx].open ? tex_door_open : tex_door_closed) : tex_secret;
            glBindTexture(GL_TEXTURE_2D, tex);
            glBegin(GL_QUADS);

            if (px < wx && is_secret_face(wx, wy, 0)) {
                float l_a = cell_light_level(wx - 1, wy);
                float l_b = cell_light_level(wx - 1, wy + 1);
                glNormal3f(-1, 0, 0);
                glTexCoord2d(0, 1); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx, 0, wy);
                glTexCoord2d(1, 1); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx, 0, wy + 1);
                glTexCoord2d(1, 0); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx, 1, wy + 1);
                glTexCoord2d(0, 0); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx, 1, wy);
            }
            if (px > wx + 1 && is_secret_face(wx, wy, 1)) {
                float l_a = cell_light_level(wx + 1, wy + 1);
                float l_b = cell_light_level(wx + 1, wy);
                glNormal3f(1, 0, 0);
                glTexCoord2d(0, 1); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx+1, 0, wy+1);
                glTexCoord2d(1, 1); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx+1, 0, wy);
                glTexCoord2d(1, 0); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx+1, 1, wy);
                glTexCoord2d(0, 0); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx+1, 1, wy+1);
            }
            if (py < wy && is_secret_face(wx, wy, 2)) {
                float l_a = cell_light_level(wx + 1, wy - 1);
                float l_b = cell_light_level(wx, wy - 1);
                glNormal3f(0, 0, -1);
                glTexCoord2d(0, 1); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx+1, 0, wy);
                glTexCoord2d(1, 1); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx, 0, wy);
                glTexCoord2d(1, 0); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx, 1, wy);
                glTexCoord2d(0, 0); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx+1, 1, wy);
            }
            if (py > wy + 1 && is_secret_face(wx, wy, 3)) {
                float l_a = cell_light_level(wx, wy + 1);
                float l_b = cell_light_level(wx + 1, wy + 1);
                glNormal3f(0, 0, 1);
                glTexCoord2d(0, 1); glColor4f(l_a, l_a, l_a, 1); glVertex3d(wx, 0, wy+1);
                glTexCoord2d(1, 1); glColor4f(l_b, l_b, l_b, 1); glVertex3d(wx+1, 0, wy+1);
                glTexCoord2d(1, 0); glColor4f(l_b*0.45f, l_b*0.45f, l_b*0.45f, 1); glVertex3d(wx+1, 1, wy+1);
                glTexCoord2d(0, 0); glColor4f(l_a*0.45f, l_a*0.45f, l_a*0.45f, 1); glVertex3d(wx, 1, wy+1);
            }
            glEnd();
        }
    }
    glDisable(GL_LIGHTING);
    glDisable(GL_COLOR_MATERIAL);
    glDisable(GL_LIGHT0);

    glDisable(GL_FOG);
}

static void render_2d_overlay(int out_w, int out_h, TTF_Font *font,
                              int minimap_on, int cutscene, Uint32 now,
                              double cutscene_dur, Uint32 state_start) {
    gl_ortho2d(0, out_w, out_h, 0);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_FOG);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    /* cutscene fade overlay */
    if (cutscene) {
        double elapsed = (now - state_start) / 1000.0;
        double t = elapsed / cutscene_dur;
        double ease = t < 0.5 ? 2.0*t*t : 1.0 - (-2.0*t+2.0)*(-2.0*t+2.0)/2.0;
        if (t < 1.0) {
            float alpha = (float)((1.0 - ease));
            glDisable(GL_TEXTURE_2D);
            glColor4f(0, 0, 0, alpha);
            glBegin(GL_QUADS);
            glVertex2f(0, 0); glVertex2f(out_w, 0);
            glVertex2f(out_w, out_h); glVertex2f(0, out_h);
            glEnd();
            glColor4f(1, 1, 1, 1);
        }
    }

    /* minimap */
    if (minimap_on && !cutscene) {
        int mw = 180, mh = 180;
        int mx0 = out_w - mw - 12;
        int my0 = 12;
        double scale = (out_w < out_h) ? (double)out_w / WIN_W : (double)out_h / WIN_H;
        int cell = (int)(6 * scale);
        int radius = 9;

        glDisable(GL_TEXTURE_2D);
        glColor4f(0, 0, 0, 0.55f);
        glBegin(GL_QUADS);
        glVertex2f(mx0 - 2, my0 - 2);
        glVertex2f(mx0 + mw * scale + 2, my0 - 2);
        glVertex2f(mx0 + mw * scale + 2, my0 + mh * scale + 2);
        glVertex2f(mx0 - 2, my0 + mh * scale + 2);
        glEnd();

        int cx = (int)px, cy = (int)py;
        for (int dy = -radius; dy <= radius; dy++) {
            for (int dx = -radius; dx <= radius; dx++) {
                if (world_cell(cx + dx, cy + dy) != 0) {
                    double sx = mx0 + (mw * scale) / 2.0 + dx * cell * scale - cell / 2.0;
                    double sy = my0 + (mh * scale) / 2.0 + dy * cell * scale - cell / 2.0;
                    glColor4f(0.24f, 0.47f, 1.0f, 0.8f);
                    glBegin(GL_QUADS);
                    glVertex2f(sx, sy);
                    glVertex2f(sx + cell, sy);
                    glVertex2f(sx + cell, sy + cell);
                    glVertex2f(sx, sy + cell);
                    glEnd();
                }
            }
        }

        double ppx = mx0 + (mw * scale) / 2.0;
        double ppy = my0 + (mh * scale) / 2.0;
        double sc = scale;
        double ax = cos(pa) * cell * 1.8;
        double ay = sin(pa) * cell * 1.8;
        glColor4f(1.0f, 0.31f, 0.31f, 1.0f);
        glLineWidth(2.0f);
        glBegin(GL_LINES);
        glVertex2f(ppx, ppy);
        glVertex2f(ppx + ax * sc, ppy + ay * sc);
        glEnd();
        double lx = -sin(pa) * cell * 0.7;
        double ly = cos(pa) * cell * 0.7;
        double hx = ppx + ax * sc, hy = ppy + ay * sc;
        glBegin(GL_LINES);
        glVertex2f(hx, hy);
        glVertex2f(hx - ax * 0.4 * sc + lx * sc, hy - ay * 0.4 * sc + ly * sc);
        glVertex2f(hx, hy);
        glVertex2f(hx - ax * 0.4 * sc - lx * sc, hy - ay * 0.4 * sc - ly * sc);
        glEnd();

        glColor4f(1, 1, 1, 1);
        glLineWidth(1.0f);
    }

    /* HUD text */
    char buf[64];
    snprintf(buf, sizeof(buf), "%.1f, %.1f", px, py);
    SDL_Color tc = {220, 220, 220, 255};
    SDL_Surface *surf = TTF_RenderUTF8_Blended(font, buf, tc);
    if (surf) {
        GLuint tid = surf_to_gl(surf);
        int tw = surf->w, th = surf->h;
        SDL_FreeSurface(surf);
        if (tid) {
            glEnable(GL_TEXTURE_2D);
            glBindTexture(GL_TEXTURE_2D, tid);
            glColor4f(1, 1, 1, 1);
            double sx = (double)out_w / WIN_W;
            double sy = (double)out_h / WIN_H;
            double dx = 10 * sx, dy = (WIN_H - 26) * sy;
            glBegin(GL_QUADS);
            glTexCoord2f(0, 0); glVertex2f(dx, dy);
            glTexCoord2f(1, 0); glVertex2f(dx + tw * sx, dy);
            glTexCoord2f(1, 1); glVertex2f(dx + tw * sx, dy + th * sy);
            glTexCoord2f(0, 1); glVertex2f(dx, dy + th * sy);
            glEnd();
            glDeleteTextures(1, &tid);
        }
    }

    /* chat messages */
    if (chat_msg_total > 0) {
        double sx = (double)out_w / WIN_W;
        double sy = (double)out_h / WIN_H;
        int count = chat_msg_total < MAX_CHAT_MSGS ? chat_msg_total : MAX_CHAT_MSGS;
        int start = chat_msg_total - count;
        int msg_h = 18;
        int max_show = 8;
        if (count > max_show) { start += count - max_show; count = max_show; }
        Uint32 now_ms = now;
        double bg_w = 480 * sx;
        for (int i = 0; i < count; i++) {
            int mi = (start + i) % MAX_CHAT_MSGS;
            Uint32 age = now_ms - chat_msg_times[mi];
            if (age > CHAT_MSG_DUR) continue;
            float alpha = 1.0f;
            if (age > CHAT_MSG_DUR - CHAT_FADE_DUR)
                alpha = 1.0f - (float)(age - (CHAT_MSG_DUR - CHAT_FADE_DUR)) / CHAT_FADE_DUR;
            double dx = 10 * sx;
            double dy = (10 + msg_h * (i + 1)) * sy;
            double msg_h_px = 16 * sy;
            /* background */
            glDisable(GL_TEXTURE_2D);
            glColor4f(0.1f, 0.1f, 0.12f, 0.85f * alpha);
            glBegin(GL_QUADS);
            glVertex2f(dx, dy); glVertex2f(dx + bg_w, dy);
            glVertex2f(dx + bg_w, dy + msg_h_px); glVertex2f(dx, dy + msg_h_px);
            glEnd();
            SDL_Color c = {255, 255, 255, (Uint8)(alpha * 255)};
            if (strstr(chat_msgs[mi], "I SEE YOU"))
                c = (SDL_Color){255, 50, 50, (Uint8)(alpha * 255)};
            SDL_Surface *surf = TTF_RenderUTF8_Blended(font, chat_msgs[mi], c);
            if (surf) {
                GLuint tid = surf_to_gl(surf);
                int tw = surf->w, th = surf->h;
                SDL_FreeSurface(surf);
                if (tid) {
                    glEnable(GL_TEXTURE_2D);
                    glBindTexture(GL_TEXTURE_2D, tid);
                    glColor4f(1, 1, 1, alpha);
                    glBegin(GL_QUADS);
                    glTexCoord2f(0, 0); glVertex2f(dx, dy);
                    glTexCoord2f(1, 0); glVertex2f(dx + tw * sx, dy);
                    glTexCoord2f(1, 1); glVertex2f(dx + tw * sx, dy + th * sy);
                    glTexCoord2f(0, 1); glVertex2f(dx, dy + th * sy);
                    glEnd();
                    glDeleteTextures(1, &tid);
                }
            }
        }
    }

    /* chat input box */
    if (chat_open) {
        double sx = (double)out_w / WIN_W;
        double sy = (double)out_h / WIN_H;
        int inp_x = 10 * sx, inp_y = 4 * sy;
        int inp_w = 300 * sx, inp_h = 22 * sy;
        glDisable(GL_TEXTURE_2D);
        glColor4f(0.1f, 0.1f, 0.12f, 0.85f);
        glBegin(GL_QUADS);
        glVertex2f(inp_x, inp_y); glVertex2f(inp_x + inp_w, inp_y);
        glVertex2f(inp_x + inp_w, inp_y + inp_h); glVertex2f(inp_x, inp_y + inp_h);
        glEnd();
        glColor4f(0.35f, 0.35f, 0.4f, 0.9f);
        glBegin(GL_LINE_LOOP);
        glVertex2f(inp_x, inp_y); glVertex2f(inp_x + inp_w, inp_y);
        glVertex2f(inp_x + inp_w, inp_y + inp_h); glVertex2f(inp_x, inp_y + inp_h);
        glEnd();

        char inp_display[300];
        snprintf(inp_display, sizeof(inp_display), "> %s%c", chat_buf, (now / 500) % 2 ? '_' : ' ');
        SDL_Color ic = {200, 200, 200, 255};
        SDL_Surface *surf = TTF_RenderUTF8_Blended(font, inp_display, ic);
        if (surf) {
            GLuint tid = surf_to_gl(surf);
            int tw = surf->w, th = surf->h;
            SDL_FreeSurface(surf);
            if (tid) {
                glEnable(GL_TEXTURE_2D);
                glBindTexture(GL_TEXTURE_2D, tid);
                glColor4f(1, 1, 1, 1);
                glBegin(GL_QUADS);
                glTexCoord2f(0, 0); glVertex2f(inp_x + 4, inp_y + 2);
                glTexCoord2f(1, 0); glVertex2f(inp_x + 4 + tw * sx, inp_y + 2);
                glTexCoord2f(1, 1); glVertex2f(inp_x + 4 + tw * sx, inp_y + 2 + th * sy);
                glTexCoord2f(0, 1); glVertex2f(inp_x + 4, inp_y + 2 + th * sy);
                glEnd();
                glDeleteTextures(1, &tid);
            }
        }

        /* X button */
        int btn_x = inp_x + inp_w + 4;
        int btn_y = inp_y;
        int btn_w = 16 * sx, btn_h = 16 * sy;
        glDisable(GL_TEXTURE_2D);
        glColor4f(0.5f, 0.15f, 0.15f, 0.85f);
        glBegin(GL_QUADS);
        glVertex2f(btn_x, btn_y); glVertex2f(btn_x + btn_w, btn_y);
        glVertex2f(btn_x + btn_w, btn_y + btn_h); glVertex2f(btn_x, btn_y + btn_h);
        glEnd();
        glColor4f(0.7f, 0.2f, 0.2f, 1.0f);
        glBegin(GL_LINE_LOOP);
        glVertex2f(btn_x, btn_y); glVertex2f(btn_x + btn_w, btn_y);
        glVertex2f(btn_x + btn_w, btn_y + btn_h); glVertex2f(btn_x, btn_y + btn_h);
        glEnd();
        glColor4f(1, 1, 1, 1);
        SDL_Surface *xs = TTF_RenderUTF8_Blended(font, "X", (SDL_Color){255,255,255,255});
        if (xs) {
            GLuint xid = surf_to_gl(xs);
            int xw = xs->w, xh = xs->h;
            SDL_FreeSurface(xs);
            if (xid) {
                glEnable(GL_TEXTURE_2D);
                glBindTexture(GL_TEXTURE_2D, xid);
                double cx = btn_x + (btn_w - xw * sx) / 2.0;
                double cy = btn_y + (btn_h - xh * sy) / 2.0;
                glColor4f(1, 1, 1, 1);
                glBegin(GL_QUADS);
                glTexCoord2f(0, 0); glVertex2f(cx, cy);
                glTexCoord2f(1, 0); glVertex2f(cx + xw * sx, cy);
                glTexCoord2f(1, 1); glVertex2f(cx + xw * sx, cy + xh * sy);
                glTexCoord2f(0, 1); glVertex2f(cx, cy + xh * sy);
                glEnd();
                glDeleteTextures(1, &xid);
            }
        }
    }

    glDisable(GL_BLEND);
    glEnable(GL_DEPTH_TEST);
    glColor4f(1, 1, 1, 1);
}

static void render_title_gl(TTF_Font *font_big, TTF_Font *font_sub,
                            Uint32 ticks) {
    int out_w, out_h;
    SDL_GL_GetDrawableSize(win, &out_w, &out_h);

    glClearColor(0.04f, 0.04f, 0.08f, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_FOG);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    gl_ortho2d(0, out_w, out_h, 0);

    /* background image as GL texture */
    if (tex_bg) {
        glEnable(GL_TEXTURE_2D);
        glBindTexture(GL_TEXTURE_2D, tex_bg);
        glColor4f(1, 1, 1, 1);
        glBegin(GL_QUADS);
        glTexCoord2f(0, 0); glVertex2f(0, 0);
        glTexCoord2f(1, 0); glVertex2f(out_w, 0);
        glTexCoord2f(1, 1); glVertex2f(out_w, out_h);
        glTexCoord2f(0, 1); glVertex2f(0, out_h);
        glEnd();
        /* dark overlay */
        glDisable(GL_TEXTURE_2D);
        glColor4f(0, 0, 0, 0.63f);
        glBegin(GL_QUADS);
        glVertex2f(0, 0); glVertex2f(out_w, 0);
        glVertex2f(out_w, out_h); glVertex2f(0, out_h);
        glEnd();
    } else {
        glDisable(GL_TEXTURE_2D);
        glColor4f(0.04f, 0.04f, 0.08f, 1.0f);
        glBegin(GL_QUADS);
        glVertex2f(0, 0); glVertex2f(out_w, 0);
        glVertex2f(out_w, out_h); glVertex2f(0, out_h);
        glEnd();
    }

    /* ASCII art title */
    double sx = (double)out_w / WIN_W;
    double sy = (double)out_h / WIN_H;
    SDL_Color white = {200, 220, 255, 255};
    int line_h = 0;
    TTF_SizeUTF8(font_big, "X", NULL, &line_h);
    int y_start = (int)(out_h * 0.18);

    glEnable(GL_TEXTURE_2D);
    for (int i = 0; i < (int)ASCII_LINES; i++) {
        int tw, th;
        TTF_SizeUTF8(font_big, ascii_art[i], &tw, &th);
        SDL_Surface *sf = TTF_RenderUTF8_Blended(font_big, ascii_art[i], white);
        if (!sf) continue;
        GLuint tid = surf_to_gl(sf);
        SDL_FreeSurface(sf);
        if (!tid) continue;
        glBindTexture(GL_TEXTURE_2D, tid);
        glColor4f(1, 1, 1, 1);
        double dx = (out_w - tw * sx) / 2.0;
        double dy = y_start + i * line_h * 1.15 * sy;
        glBegin(GL_QUADS);
        glTexCoord2f(0, 0); glVertex2f(dx, dy);
        glTexCoord2f(1, 0); glVertex2f(dx + tw * sx, dy);
        glTexCoord2f(1, 1); glVertex2f(dx + tw * sx, dy + th * sy);
        glTexCoord2f(0, 1); glVertex2f(dx, dy + th * sy);
        glEnd();
        glDeleteTextures(1, &tid);
    }

    /* pulsing prompt */
    double pulse = 0.5 + 0.5 * sin(ticks * 0.003);
    Uint8 alpha = (Uint8)(120 + (int)(pulse * 135));
    SDL_Color subc = {200, 200, 200, 255};
    const char *prompt = "press anywhere to play";
    int pw, ph;
    TTF_SizeUTF8(font_sub, prompt, &pw, &ph);
    SDL_Surface *ps = TTF_RenderUTF8_Blended(font_sub, prompt, subc);
    if (ps) {
        GLuint ptid = surf_to_gl(ps);
        SDL_FreeSurface(ps);
        if (ptid) {
            glBindTexture(GL_TEXTURE_2D, ptid);
            glColor4f(1, 1, 1, (float)alpha / 255.0f);
            double pdx = (out_w - pw * sx) / 2.0;
            double pdy = out_h * 0.82;
            glBegin(GL_QUADS);
            glTexCoord2f(0, 0); glVertex2f(pdx, pdy);
            glTexCoord2f(1, 0); glVertex2f(pdx + pw * sx, pdy);
            glTexCoord2f(1, 1); glVertex2f(pdx + pw * sx, pdy + ph * sy);
            glTexCoord2f(0, 1); glVertex2f(pdx, pdy + ph * sy);
            glEnd();
            glDeleteTextures(1, &ptid);
        }
    }

    glColor4f(1, 1, 1, 1);
    glDisable(GL_BLEND);
}

int main(int argc, char *argv[]) {
    (void)argc; (void)argv;
    srand((unsigned)time(NULL));
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    IMG_Init(IMG_INIT_PNG);
    TTF_Init();
    Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 512);
    Mix_AllocateChannels(4);

    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    win = SDL_CreateWindow("Complexx Raycaster",
        SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
        WIN_W, WIN_H, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL);
    SDL_GLContext gl_ctx = SDL_GL_CreateContext(win);
    SDL_GL_SetSwapInterval(1);

    TTF_Font *font = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSansMono.ttf", 14);
    if (!font) font = TTF_OpenFont("/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf", 14);
#ifdef _WIN32
    if (!font) font = TTF_OpenFont("C:/Windows/Fonts/consola.ttf", 14);
    if (!font) font = TTF_OpenFont("C:/Windows/Fonts/lucon.ttf", 14);
#endif

    char exe_path[512];
    char base_dir[512] = ".";
#ifdef _WIN32
    DWORD plen = GetModuleFileNameA(NULL, exe_path, sizeof(exe_path) - 1);
    if (plen > 0) {
        exe_path[plen] = '\0';
        char *s = strrchr(exe_path, '\\');
        if (s) { *s = '\0'; s = strrchr(exe_path, '\\'); }
        if (s) { *s = '\0'; strcpy(base_dir, exe_path); }
    }
#else
    ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
    if (len > 0) {
        exe_path[len] = '\0';
        char *s = strrchr(exe_path, '/');
        if (s) { *s = '\0'; s = strrchr(exe_path, '/'); }
        if (s) { *s = '\0'; strcpy(base_dir, exe_path); }
    }
#endif

    char iconpath[600];
    snprintf(iconpath, sizeof(iconpath), "%s/assets/images/icons/icon.ico", base_dir);
    SDL_Surface *icon = IMG_Load(iconpath);
    if (icon) { SDL_SetWindowIcon(win, icon); SDL_FreeSurface(icon); }

    char bgimgpath[600];
    snprintf(bgimgpath, sizeof(bgimgpath), "%s/assets/images/bgimage.png", base_dir);

    char fpath[600];
    snprintf(fpath, sizeof(fpath), "%s/assets/fonts/SourceCodePro-Bold.ttf", base_dir);
    TTF_Font *font_big = TTF_OpenFont(fpath, 18);
    if (!font_big) font_big = TTF_OpenFont("/usr/share/qtcreator/fonts/SourceCodePro-Bold.ttf", 18);
#ifdef _WIN32
    if (!font_big) font_big = TTF_OpenFont("C:/Windows/Fonts/courbd.ttf", 18);
    if (!font_big) font_big = TTF_OpenFont("C:/Windows/Fonts/consolab.ttf", 18);
#endif
    if (!font_big) font_big = font;
    TTF_Font *font_sub = TTF_OpenFont(fpath, 28);
    if (!font_sub) font_sub = TTF_OpenFont("/usr/share/qtcreator/fonts/SourceCodePro-Bold.ttf", 28);
#ifdef _WIN32
    if (!font_sub) font_sub = TTF_OpenFont("C:/Windows/Fonts/courbd.ttf", 28);
    if (!font_sub) font_sub = TTF_OpenFont("C:/Windows/Fonts/consolab.ttf", 28);
#endif
    if (!font_sub) font_sub = font;

    Mix_Chunk *step_snd = NULL;
    char steppath[600];
    snprintf(steppath, sizeof(steppath), "%s/assets/sounds/step5.wav", base_dir);
    step_snd = Mix_LoadWAV(steppath);

    char bgpath[600];
    snprintf(bgpath, sizeof(bgpath), "%s/assets/sounds/bgmusic.mp3", base_dir);
    Mix_Music *bgmusic = Mix_LoadMUS(bgpath);
    if (bgmusic) Mix_PlayMusic(bgmusic, -1);

    char wp[600], sp[600], fp[600], cp[600];
    snprintf(wp, sizeof(wp), "%s/assets/images/rooms/level_0/wall_brick.png", base_dir);
    snprintf(sp, sizeof(sp), "%s/assets/images/rooms/level_0/wall_brick_secret.png", base_dir);
    snprintf(fp, sizeof(fp), "%s/assets/images/rooms/level_0/floor_.png", base_dir);
    snprintf(cp, sizeof(cp), "%s/assets/images/rooms/level_0/ceiling_.png", base_dir);

    tex_wall = load_gl_tex(wp);
    tex_secret = load_gl_tex(sp);
    tex_floor_id = load_gl_tex(fp);
    tex_ceil = load_gl_tex(cp);
    tex_bg = load_gl_tex(bgimgpath);
    if (!tex_wall || !tex_secret || !tex_floor_id || !tex_ceil) {
        fprintf(stderr, "Missing textures\n"); return 1;
    }

    char dp0[600], dp80[600];
    snprintf(dp0, sizeof(dp0), "%s/assets/images/rooms/level_0/wall_brick_secret_door0.png", base_dir);
    snprintf(dp80, sizeof(dp80), "%s/assets/images/rooms/level_0/wall_brick_secret_door80.png", base_dir);
    tex_door_closed = load_gl_tex(dp0);
    tex_door_open   = load_gl_tex(dp80);

    char curpath[600];
    snprintf(curpath, sizeof(curpath), "%s/assets/images/icons/cursors/arrow.cur", base_dir);
    SDL_Surface *cur_surf = IMG_Load(curpath);
    if (!cur_surf) cur_surf = IMG_Load("assets/images/icons/cursors/arrow.cur");
    SDL_Cursor *menu_cursor = cur_surf ? SDL_CreateColorCursor(cur_surf, 0, 0) : NULL;
    if (cur_surf) SDL_FreeSurface(cur_surf);

    player_init();
    while (is_solid(px, py)) px += 1.0;

    Mix_VolumeMusic(0);
    SDL_SetRelativeMouseMode(SDL_FALSE);
    if (menu_cursor) SDL_SetCursor(menu_cursor);
    SDL_ShowCursor(SDL_ENABLE);
    int mouse_look = 0, running = 1, minimap_on = 0;
    Uint32 last = SDL_GetTicks();
    Uint32 last_step = 0;

    int state = STATE_TITLE;
    Uint32 state_start = SDL_GetTicks();
    double cutscene_dur = 3.5;

    while (running) {
        Uint32 now = SDL_GetTicks();
        double dt = (now - last) / 1000.0;
        last = now;
        if (dt > 0.05) dt = 0.05;

        SDL_Event ev;
        while (SDL_PollEvent(&ev)) {
            if (ev.type == SDL_QUIT) running = 0;

            if (state == STATE_TITLE) {
                if (ev.type == SDL_KEYDOWN || ev.type == SDL_MOUSEBUTTONDOWN) {
                    state = STATE_CUTSCENE;
                    state_start = SDL_GetTicks();
                    mouse_look = 1;
                    SDL_SetRelativeMouseMode(SDL_TRUE);
                }
                continue;
            }

            if (ev.type == SDL_KEYDOWN) {
                if (chat_open) {
                    if (ev.key.keysym.sym == SDLK_RETURN || ev.key.keysym.sym == SDLK_KP_ENTER) {
                        if (chat_buf_len > 0) {
                            chat_buf[chat_buf_len] = 0;
                            chat_add(chat_buf);
                            chat_buf_len = 0;
                        }
                    } else if (ev.key.keysym.sym == SDLK_BACKSPACE) {
                        if (chat_buf_len > 0) chat_buf_len--;
                    } else if (ev.key.keysym.sym == SDLK_ESCAPE) {
                        chat_open = 0;
                        SDL_StopTextInput();
                        mouse_look = chat_mouse_look_saved;
                        if (mouse_look) { SDL_SetRelativeMouseMode(SDL_TRUE); SDL_ShowCursor(SDL_DISABLE); }
                        else { SDL_SetRelativeMouseMode(SDL_FALSE); SDL_ShowCursor(SDL_ENABLE); }
                    }
                } else {
                    if (ev.key.keysym.sym == SDLK_ESCAPE) {
                        if (mouse_look) { SDL_SetRelativeMouseMode(SDL_FALSE); mouse_look = 0; SDL_ShowCursor(SDL_ENABLE); }
                        else running = 0;
                    }
                    if (ev.key.keysym.sym == SDLK_TAB) {
                        SDL_SetRelativeMouseMode(mouse_look ? SDL_FALSE : SDL_TRUE);
                        mouse_look = !mouse_look;
                        SDL_ShowCursor(mouse_look ? SDL_DISABLE : SDL_ENABLE);
                    }
                    if (ev.key.keysym.sym == SDLK_f || ev.key.keysym.sym == SDLK_F11) {
                        Uint32 f = SDL_GetWindowFlags(win);
                        SDL_SetWindowFullscreen(win, f & SDL_WINDOW_FULLSCREEN_DESKTOP ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP);
                    }
                    if (ev.key.keysym.sym == SDLK_m) minimap_on = !minimap_on;
                    if (ev.key.keysym.sym == SDLK_q) { flashlight_on = !flashlight_on; flash_vel = 8.0; }
                    if (ev.key.keysym.sym == SDLK_t && state == STATE_PLAY) {
                        chat_open = 1;
                        chat_mouse_look_saved = mouse_look;
                        mouse_look = 0;
                        SDL_SetRelativeMouseMode(SDL_FALSE);
                        SDL_ShowCursor(SDL_ENABLE);
                        SDL_StartTextInput();
                    }
                }
            }
            if (ev.type == SDL_TEXTINPUT && chat_open) {
                if (chat_buf_len < 255) {
                    chat_buf[chat_buf_len++] = ev.text.text[0];
                }
            }
            if (ev.type == SDL_MOUSEMOTION && mouse_look) {
                player_rotate(ev.motion.xrel * 0.003);
                cam_pitch -= ev.motion.yrel * 0.003;
                if (cam_pitch >  M_PI / 2.0) cam_pitch =  M_PI / 2.0;
                if (cam_pitch < -M_PI / 2.0) cam_pitch = -M_PI / 2.0;
            }
            if (ev.type == SDL_MOUSEBUTTONDOWN && ev.button.button == SDL_BUTTON_LEFT) {
                if (chat_open) {
                    int out_w, out_h;
                    SDL_GL_GetDrawableSize(win, &out_w, &out_h);
                    double sx = (double)out_w / WIN_W;
                    double sy = (double)out_h / WIN_H;
                    int inp_x = 10 * sx, inp_y = 4 * sy;
                    int inp_w = 300 * sx;
                    int btn_x = inp_x + inp_w + 4;
                    int btn_y = inp_y;
                    int btn_w = 16 * sx, btn_h = 16 * sy;
                    int mx = ev.button.x, my = ev.button.y;
                    if (mx >= btn_x && mx <= btn_x + btn_w &&
                        my >= btn_y && my <= btn_y + btn_h) {
                        chat_open = 0;
                        SDL_StopTextInput();
                        mouse_look = chat_mouse_look_saved;
                        if (mouse_look) { SDL_SetRelativeMouseMode(SDL_TRUE); SDL_ShowCursor(SDL_DISABLE); }
                        else { SDL_SetRelativeMouseMode(SDL_FALSE); SDL_ShowCursor(SDL_ENABLE); }
                    }
                } else if (state == STATE_PLAY && mouse_look) {
                    int best = -1;
                    double best_d = 4.0;
                    for (int i = 0; i < door_count; i++) {
                        if (!doors[i].valid) continue;
                        double dx = doors[i].wx + 0.5 - px;
                        double dy = doors[i].wy + 0.5 - py;
                        double d = dx * dx + dy * dy;
                        if (d < best_d) { best_d = d; best = i; }
                    }
                    if (best >= 0) doors[best].open = !doors[best].open;
                }
            }
        }

        if (state == STATE_TITLE) {
            render_title_gl(font_big, font_sub, now);
            SDL_GL_SwapWindow(win);
            continue;
        }

        int cutscene = (state == STATE_CUTSCENE);

        /* zalgo messages from the dark */
        if (state == STATE_PLAY && !cutscene) {
            int cell_x = (int)floor(px), cell_y = (int)floor(py);
            if (is_dark_cell(cell_x, cell_y) && flash_spring < 0.1 && (rand() % 100) < 60) {
                char zalgo[128];
                make_zalgo(zalgo, sizeof(zalgo), "zalgo");
                char msg[600];
                snprintf(msg, sizeof(msg), "[{%s}]: I SEE YOU", zalgo);
                chat_add(msg);
            }
        }

        if (!cutscene && !chat_open) {
            const Uint8 *k = SDL_GetKeyboardState(NULL);
            int sprinting = k[SDL_SCANCODE_LSHIFT] || k[SDL_SCANCODE_RSHIFT];
            double spd = (sprinting ? SPRINT_SPEED : MOVE_SPEED) * dt;
            double fwd = 0, str = 0;
            if (k[SDL_SCANCODE_W] || k[SDL_SCANCODE_UP])    fwd += spd;
            if (k[SDL_SCANCODE_S] || k[SDL_SCANCODE_DOWN])  fwd -= spd;
            if (k[SDL_SCANCODE_A]) str -= spd;
            if (k[SDL_SCANCODE_D]) str += spd;
            if (k[SDL_SCANCODE_LEFT])  player_rotate(-ROT_SPEED * dt);
            if (k[SDL_SCANCODE_RIGHT]) player_rotate(ROT_SPEED * dt);
            player_move(fwd, str);

            int moving = (fwd != 0 || str != 0);
            if (moving && sprinting) bob_t += SPRINT_BOB_SPEED * dt;
            else bob_t = 0;

            if (moving) {
                int cooldown = sprinting ? 200 : 600;
                if (now - last_step > (Uint32)cooldown) {
                    if (step_snd) Mix_PlayChannel(-1, step_snd, 0);
                    last_step = now;
                }
            }
        }

        if (cutscene) {
            double elapsed = (now - state_start) / 1000.0;
            double t = elapsed / cutscene_dur;
            if (t >= 1.0) { t = 1.0; state = STATE_PLAY; cam_pitch = 0.0; cam_y = 0.5; SDL_ShowCursor(SDL_DISABLE); }
            double ease = t < 0.5 ? 2.0*t*t : 1.0 - (-2.0*t+2.0)*(-2.0*t+2.0)/2.0;
            cam_pitch = 0.8 * (1.0 - ease);
            cam_y = 0.1 + 0.4 * ease;
            Mix_VolumeMusic((int)(ease * 128));
        }

        /* flashlight spring */
        double flash_target = flashlight_on ? 1.0 : 0.0;
        double flash_accel = -200.0 * (flash_spring - flash_target) - 20.0 * flash_vel;
        flash_vel += flash_accel * dt;
        flash_spring += flash_vel * dt;
        if (flash_spring < 0.0) flash_spring = 0.0;

        int out_w, out_h;
        SDL_GL_GetDrawableSize(win, &out_w, &out_h);

        render_3d();
        render_2d_overlay(out_w, out_h, font, minimap_on, cutscene, now, cutscene_dur, state_start);
        SDL_GL_SwapWindow(win);
    }

    Mix_FreeChunk(step_snd);
    Mix_FreeMusic(bgmusic);
    Mix_CloseAudio();
    if (menu_cursor) SDL_FreeCursor(menu_cursor);
    if (font) TTF_CloseFont(font);
    if (font_big && font_big != font) TTF_CloseFont(font_big);
    if (font_sub && font_sub != font) TTF_CloseFont(font_sub);
    TTF_Quit(); IMG_Quit();
    SDL_GL_DeleteContext(gl_ctx);
    SDL_DestroyWindow(win);
    SDL_Quit();
    return 0;
}
